Whoever talks about the advantages of MVP also talks about unit-tests without using graphic tools like Selenium (for web-browsers) or fest (for Java/Swing).
These robot-tools would find a button with a certain identity and click on it, or write into a text-field. But they depend on an operating-system with a graphics environment, they would not run on a "headless" machine like your build-server might be.
This Blog presents such a unit test for the TemperatureMvp
application that I introduced in my recent article series. It is written in Java, using JUnit (annotation-based).
To exclude the graphics environment from the test, we need to "mock" (imitate) the view and event-listener interfaces. That way the test will not depend on any windowing-system. Keeping the view passive would guarantee that any test-worthy presentation-logic is in the presenter or model. Thus the test can be considered to be efficient!
Here is a Java unit test skeleton that provides model, view and presenter in private fields.
1 | import org.junit.*; |
This test base-class is abstract because I won't provide a ViewMock
implementation here, I will do that in a test class derived from this one. The inner interface ViewMock
describes anything the test needs to work. It extends TemperatureView
for an abstracted windowing-system W
.
For testing we need more methods than the TemperatureView
interfaces provide. We want to programmatically write into a text field and then press ENTER. The inputCelsius()
and inputFahrenheit()
methods will do that. Further we want to directly read the contents of the text fields, to check whether they are consistent with the model. The getCelsius()
and getFahrenheit()
methods will give us that.
So we can declare an protected abstract method that creates a concrete ViewMock
implementation, work with that inside the abstract test, but leave the implementation to sub-classes. This is done by the newViewMock()
method.
JUnit 4 tests do not derive a TestCase
any more, they have @Test
annotations on public test-methods, and instead of a setUp()
override you put a @Before
on some set-up method.
The beforeTest()
method builds the MVP. It uses newViewMock()
to create the view. The model is then initialized with INITIAL_CELSIUS
value and set into the presenter. The test methods will work with these private model, view and presenter fields.
Now we need concrete tests. I will place them in the abstraction, so that I can use them with different mock-concepts. Here is the flesh for our skeleton:
1 | import org.junit.*; |
The testEmptyModel()
method tests that all values are null after setting an empty model into the presenter.
The testInitialModel()
asserts the initial values set by beforeTest()
.
The testChangeTemperatures()
changes first the Celsius field and asserts that Fahrenheit is present and correct, then it does the same with the Fahrenheit field.
I believe that these tests cover most of the functionality implemented in the temperature-converter. Maybe we should also do negative values.
Now the hairy part starts. We need to implement a class that imitates the view, including the listener mechanism. Here is the concrete class that you can run as unit test, containing a ViewMock
implementation.
1 | import java.util.*; |
It's a fact that all unit tests somehow duplicate application code. In this case it is becoming a little painful, because the number of fields in a real-world application is much higher than in this minimal temperature-MVP. Not only that there will be 30-40 of them, they will also be of complex structure like choosers, or be inside tables or trees.
Here we have three methods per field:
getCelsius()
(test helper, not in view-interface)setTemperatureInCelsius()
(from view interface)inputCelsius()
(test helper to simulate input, not in view-interface)So you would have to implement 90 methods in case your UI has 30 fields!
The view mock implementation makes headless MVP unit tests a little cumbersome. This would require a generic solution, or some intelligent mock-library that can associate a bean-field with its getter and setter.
I dared to make it a little easier by reusing the Vaadin view implementation for that, and it actually worked, but there is no guarantee that this can be done with every windowing-system. Moreover I had to make some things public in the view, just for the test.
1 | import vaadin.examples.mvp.model2views.viewimpl.VaadinTemperatureView; |
Such a view-mock makes clear that a view needs to be "passive". In case the real UI view contained any logic, the unit test would not meet its target (to test the UI), because the mock does not contain any logic except setters, getters and the listener mechanism. For the unit test to be realistic, both mock and real view must implement the same behavior.
Still on my Blog schedule:
ɔ⃝ Fritz Ritzberger, 2017-02-21