27 March 2020

TDD an Immediate Mode UI

Today I continue my experiments with test driving user interfaces. First I looked at test driving classic, fat, state based UIs like Swing or WinForms and later web component libraries like Vaadin. I need to explore different programming environments and platforms to see what is possible. In this post I want to have a look at immediate mode GUIs. An immediate mode GUI is a GUI where the event processing is directly controlled by the application. When I first read a similar sentence on Wikipedia, it did not help me at all. So what is an immediate mode GUI?

User InterfaceRetained 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.
For Swing I cannot see how this approach would be practical, but in this example with immediate mode raylib, it feels natural.

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 Rectangles later.
  • Use abstract coordinates, i.e. map my coordinates into raylib Rectangles. 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.
The wrapper for raylib is straight forward.
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.

Water PressureConclusion
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!

No comments: