GUI Testing using MUnit in Smallworld
In Smallworld development it becomes, increasingly more, common practice to use MUnit for automated testing. Advanced teams plug MUnit into Jenkins to create automated continuous integration.
One of the remaining challenges is testing the graphical user interface. Sure there are many automated GUI robots out there that simulate mouse events and use OCR for result validation. But in reality this is often very tedious and error-prone work. Changes to the software can easily break the test and require a new recording session.
Presentation Model
A promising solution is the so-called Presentation Model design pattern.
The three parts of the pattern define the behavior of a graphical user interface.
The Presentation Model holds the state and defines the logic of the GUI. For example the presentation model determines if a control is enabled or disabled and it executes the actions of the controls.
The Dialog defines the layout of the GUI and the visual aspects like size and position.
The Engine holds the business domain knowledge and actually performs the tasks the user is interested in.
This pattern becomes interesting for testing if the binding between the Presentation Model and the Dialog is very strong. If the binding is strong then validating the Presentation Model also validates the Dialog!
sw_action
Luckily such a strong binding is available in Smallworld: the sw_action. The sw_action is an abstraction of a gui control and is created without a GUI. It can be executed, assigned a value, assigned a list, assigned a picture, etc., all without a GUI. The state is handled in the sw_action object and the execution is done by the engine that is assigned to the sw_action. Controls can be bound to the sw_action to visualize the state and to manipulate it.
In this example the user clicks a button. That triggers the first sw_action that executes on the engine. The engine sets the value in the second sw_action that is bound to a text_item that displays the result.
This particular use case can be validated without creating any GUI controls. The test_case can execute the first sw_action and read the value of the second sw_action.
If the test runs successfully, then it is reasonable to assume that the dialog will function correctly also.
Example
As an example let’s build a fictional dialog that loads selected objects in a list. In the list the objects can be selected, toggled on/off and given a custom name.
Eventually the GUI will look like the picture below, but that is of no concern at the moment. First we test state and logic.
Using the Test Driven Development principle we will start by creating a test first by subclassing the base class test_case.
_pragma(classify_level=basic, topic={test}) def_slotted_exemplar(:selection_reporter_dialog_test_case, ## Test the behavior of the presentation model. { {:pmodel, _unset}, {:report_data, _unset}, {:report?, _unset} }, :test_case) $
For each test we setup the environment by creating a presentation_model that uses a (mocked) engine.
_pragma(classify_level=restricted,topic={test}) _method selection_reporter_dialog_test_case.set_up() _local engine << _self.mock_engine() _local pmodel << selection_reporter_pmodel.new(engine) .pmodel << pmodel _endmethod $
First we start by testing that on the initial startup most of the buttons will be disabled. We simply do this by accessing the actions from the presentation model.
_pragma(classify_level=basic, topic={test}) _method selection_reporter_dialog_test_case.test_initially_disabled_states() ## Test that when the GUI is activated initially, the ok, add ## and del buttons are greyed out. # _local as << .pmodel.actions _self.assert_false(as[:ok].enabled?, "When activated, the dialog OK button is disabled") _self.assert_false(as[:add].enabled?, "When activated, the dialog Add button is disabled") _self.assert_false(as[:del].enabled?, "When activated, the dialog Del button is disabled") _endmethod $
Next we create the presentation model. We create actions as an abstraction of all the GUI controls.
_pragma(classify_level=basic, topic={test}) _private _method selection_reporter_pmodel.init_actions() ## Intialise actions. _self.add_action(:filename, :engine, _self, :dialog_control, :text_item, :value_change_message, :|filename_changed()|, :incremental_change_message, :|filename_changed()|) _self.add_action(:add, :engine, _self, :action_message, :|add()|, :image, {:element_add, :ui_resources}) _self.add_tree_action(:tree, _self, :data, rope.new(), :selection, {}, :value_changed_notifier, :|tree_value_changed()|, :toggle_notifier, :|tree_toggle_changed()|, :select_notifier, :|tree_selection_changed()|) ... _endmethod $
Now we make sure that the test passes, by managing the actions after each event.
_pragma(classify_level=basic, topic={test}) _method selection_reporter_pmodel.manage_actions() _local t << .actions[:tree] .actions[:select_all].enabled? << .selection.size <> .data.size .actions[:check_all].enabled? << _self.toggle_count <> .data.size .actions[:add].enabled? << .map_selection.empty?.not .actions[:del].enabled? << .selection.empty?.not .actions[:ok].enabled? << .data.empty?.not _and .filename _isnt _unset _andif .filename.empty?.not _endmethod $
Next we move on to more complex functionality. When something is selected in the map, then the Add button should be enabled. After execution the add() the tree should be filled with data. This we can test by simulating a map selection. The MUnit framework has powerful mocking constructions to do simulate that.
_pragma(classify_level=basic, topic={test}) _method selection_reporter_dialog_test_case.test_add() ## Test that when something is selected in the map, the add ## button will be enabled. After pressing the add button the ## tree_item should be populated. # _local as << .pmodel.actions _self.simulate_map_selection() _self.assert_false(as[:ok].enabled?, "When activated, the dialog OK button is disabled") _self.assert_true(as[:add].enabled?, "When something is selected in the map, The Add button should be pressed") _self.assert_false(as[:del].enabled?, "When activated, the dialog Del button is disabled") as[:add].execute_action() _self.assert_true(as[:tree].data.empty?.not, "After pressing Add, the list should be populated") _endmethod $
And again we add functionality to the presentation model to make sure that the test passes.
_pragma(classify_level=basic, topic={test}) _method selection_reporter_pmodel.add() ## Add the map_selection to the data. # _for i_rwo _over .map_selection.rwo_set().fast_elements() _loop .data[i_rwo] << property_list.new_with( :on, _true, :name, i_rwo.external_name) _endloop .actions[:tree].renew_data(_self.trees) _self.update_toggle_count() _self.manage_actions() _endmethod $
We can go on like this until all functionality of the model is implemented and tested.
Next, as the final step, we will create an actual dialog with GUI controls for the user. The dialog will be a subclass of model, as usual, and will have the presentation model as a slot. The actions of the presentation model will be placed as GUI controls on the panel.
_pragma(classify_level=basic, topic={test}) _private _method selection_reporter_dialog.build_list_buttons_panel(p_container) _local rc << rowcol.new(p_container, 1, _unset, _unset, :style, :button_box) _self.actions[:add].place_control_on(rc, :dialog) _self.actions[:del].place_control_on(rc, :dialog) _self.actions[:select_all].place_control_on(rc, :dialog) _endmethod $
The dialog will not handle any logic, that is all handled by the presentation model. The dialog will only create the GUI and handle the interaction with the application plugin.
The complete sourcecode is available at: https://github.com/FrankVanHam/blog/tree/master/gui_test_product.
Conclusion
Each class in the design pattern has a clear and single responsibility. There is no duplication of code necessary. The Presentation Model design pattern offers a robust and elegant approach to build and test user interfaces in Smallworld.
I like it, do you?









