Retained Mode
A better question would be "what is retained mode?" Wikipedia states that retained mode is a style of API design in which the graphics library retains the complete object model of the rendering primitives to be rendered. That means that the widget, e.g. an instance of a
JButton
, contains all state needed to draw the button, i.e. colours, position, is the button clicked and so forth. When the button is created, or even "drawn", it is not causing the actual rendering. The GUI library decides when and how to render the widget and optimises the actual rendering. This includes double buffering, clipping or partial updates. Retained mode is the dominant style in GUI libraries, all user interfaces you have build were likely of this style.Immediate Mode
So let's get back to Immediate mode. When using an immediate mode GUI library, the event processing is directly controlled by the application. There is no button object, there is just a
Button(bounds Rectangle, text string)
function which immediately draws the button with given text at the given position and size (argument bounds
). The function returns true
if the button was clicked. The application code must call all drawing commands required to describe the entire scene each time a new frame is displayed. This is often used in video games programming and examples of immediate mode rendering systems include Direct2D and OpenGL. If you want to know more about this mode, see this list of immediate mode gui tutorials on StackOverflow.The (immediate mode) UI framework I am going to use is raylib, a simple and easy to use library to enjoy video games programming. See its cheat sheet for an overview of its functions. It has a simple API which hides everything regarding windowing system and environment. I am writing code in Go, so I am using raylib-go, the Golang bindings for raylib. Honestly I have no idea what I am doing. I have never used an immediate mode framework, not even heard of one before last year. In addition I know little to nothing about the Go language. Nevertheless I managed to talk my fellow crafter, Extreme Programmer and probably Vienna's longest time Go practitioner, Christian Haas into running this experiment with me. We spent one full day working on the Login Form exercise.
The first test: There is a button
import ( "testing" ... rl "github.com/gen2brain/raylib-go/raylib" ) func TestForm_LoginButton(t *testing.T) { var form login.Form ui := newTestingUI() form.Render(ui) if !ui.buttonCalled { t.Errorf("Button() was not called") } }
Form
is the struct
containing the form's data, which is empty for now. Render
is a receiver function on the form, which creates a button with no bounds and an empty text.type Form struct{} func (form Form) Render(ui FormUI) { ui.Button(rl.Rectangle{}, "") }To check that certain calls into raylib have been made, there is an interface
FormUI
between the application code and raylib. In the tests this interface is mocked to verify certain calls have been made. (In Go an interface type is defined as a set of method signatures. This is the way to achieve polymorphism.)type testingUI struct { buttonCalled bool } func (ui *testingUI) Button(bounds rl.Rectangle, text string) bool { ui.buttonCalled = true return false }This follows an approach I have found as a possible TDD approach:
- Design and write your methods separated from the actual UI.
- TDD the elements and behaviour.
- Mock single UI elements to verify necessary calls but do not show them.
More Code
Soon the number of interactions with the UI made it necessary to add string ids to each drawing primitive. While raylib did not need them, other libaries do, so it did not feel wrong to add them. The mocked UI was growing. Here are the final pieces of code for the login button.
type testingUI struct { // verify if Button method has been called (mock) buttonCalled map[string]bool // record button's text and bounds for later inspection (spy) buttonText map[string]string buttonBounds map[string]rl.Rectangle // return value of the Button method = user interaction (stub) buttonResults map[string]bool ... } func newTestingUI() *testingUI { ui := &testingUI{ buttonCalled: make(map[string]bool), buttonText: make(map[string]string), buttonBounds: make(map[string]rl.Rectangle), buttonResults: make(map[string]bool), ... } return ui } func (ui *testingUI) Button(id string, bounds rl.Rectangle, text string) bool { ui.buttonCalled[id] = true ui.buttonText[id] = text ui.buttonBounds[id] = bounds result := ui.buttonResults[id] ui.buttonResults[id] = false // reset button click after first call return result } func TestForm_LoginButton(t *testing.T) { var form login.Form ui := newTestingUI() form.Render(ui) if !ui.buttonCalled["login"] { t.Errorf("not found") } } func TestForm_LoginButtonText(t *testing.T) { var form login.Form ui := newTestingUI() form.Render(ui) if "Log in" != ui.buttonText["login"] { t.Errorf("is not \"Log in\"") } } func TestForm_LoginButtonBounds(t *testing.T) { var form login.Form ui := newTestingUI() form.Render(ui) expectedBounds := rl.Rectangle{300, 165, 110, 30} if ui.buttonBounds["login"] != expectedBounds { t.Errorf("expected %v, but was %v", expectedBounds, ui.buttonBounds) } }and the production code
type FormUI interface { Button(id string, bounds rl.Rectangle, text string) bool ... } func (form *Form) Render(ui FormUI) bool { buttonBounds := rl.Rectangle{X: 300, Y: 165, Width: 110, Height: 30} if ui.Button("login", buttonBounds, "Log in") { // TODO authenticate } return false }The third test,
TestForm_LoginButtonBounds
checks the position and size of the button. These properties are considered "layout". I do not like to test layout. I had to open GIMP to decide on the proper rectangle in expectedBounds
, which I really dislike. I also expect this values to change a lot during initial development. Additionally Rectangle
is a raylib type and so we depend on raylib in our code. Other options would have been:- Ignore layout completely. But then we would need to revisit all calls and add the
Rectangle
s later. - Use abstract coordinates, i.e. map my coordinates into raylib
Rectangle
s. That seemed like an extra overhead. - Move the responsibility of layout into the wrapper. There would be a button method for each button in the application and there would be more code outside my tests.
- Move out the bounds and store them in the wrapper with a simple lookup on the id. Moving out stuff is against the nature of Immediate mode because the whole UI is expected to be in the code.
type RaylibFormUI struct{} func (ui *RaylibFormUI) Button(id string, bounds rl.Rectangle, text string) bool { return raygui.Button(bounds, text) }This should give you an idea how things worked out. If you want to follow our TDD steps, here are the individual commits.
Is this MVP?
The MVP (Model View Presenter pattern) has a very thin, dumb UI, which is called the view. The model contains the UI data or UI model which might contain information about enabled fields, active buttons and so on. The presenter is a mediator and wires the model and the view. In the Go code above, the
Form
structure could be seen as an UI model. Later it will hold the user name and password data. The form receiver function func (form *Form) Render(ui FormUI) bool
contains the presenter logic. It is not a separate object - there are no objects in Go - but it could be separate. Due to the immediate mode UI, there are no callbacks from the view in case of events. This removes the need for the usual MVP event listeners. The FormUI
interface is like a MVP view as it hides the raylib dependency. It does not abstract away the underlying library, it is just a thin wrapper. It is not a MVP view. It could be made a view, i.e. provide more abstract functions in domain language, and then it would need tests on its own. In the experiment, this seemed unnecessary. In the end the FormUI
will delegate many functions to raylib, so it could be generated from its original source code. This shows the tight coupling of the FormUI
and the underlying UI library.Conclusion
We built a login user interface following the requirements using Test Driven Development. It was easy and we did not face any problems, so no big deal. The Immediate mode library made it easier than for retained mode: There was no need to search for components, capture events, trigger events and so forth, definitely easier than my initial experiment. When comparing these experiments for Swing and raylib, I am reminded of the difference of Classic and Mockist TDD. In retained mode, e.g. Swing, I kept checking the state of UI components while in immediate mode I verified expected calls of the library. This approach allows variable depth of checks. We could have asserted colours and styles and we did not. So we will have to look at the finished form in the end. Christian proposed saving the image of the final view and storing it for regression testing.
We used TDD but there was little pressure on the design of the code. There was some pressure on the design of the API of
Form
, but there was no pressure on its internal workings nor on FormUI
at all. The tests drove the creation of the UI - which was dictated by the requirements - there was no space for evolution. A different (UI) design might have been easier to test, but that was not an option. (A different UI would not have been different to test in this example anyway.) So we lost this particular benefit of TDD.Try it yourself
I will continue my experiments and I would like to hear your ideas on the topic. The Go starting code with raylib-Go, its required dependencies and linter setup is available in the Login Form Kata. Try it yourself!
1 comment:
thnks a lot it helped
Post a Comment