The hierarchical model-view-controller example of my latest Blog needs a unit-test. This time I will not hand-code the view-mock any more, I will use the Mockito library that can create any mock generically at runtime.
When using a mock-library, you will not receive "I am silly" from mock.getWhatIAm()
after calling mock.setWhatIAm("I am silly")
. Generally a generic mock can not know how it should behave when a certain method is called. But mock libraries allow to teach a mock how to behave.
Let me outline the new unit test for the hierarchical MeasurementStation MVC. You may remember that it provided a "Location" field, and contained a nested Temperature MVC with two "Celsius" and "Fahrenheit" fields that interact with each other. That presentation logic I want to test now.
This test is called MeasurementStationMcTest
because it does not test the view (missing "v"). It uses a mock object instead of a view, and thus just M odel and C ontroller are tested by "XxxMc Test".
Please find the source of the referenced MVC classes in my passed Blog articles about MVC.
1 | package vaadin.examples.mvc.hierarchical; |
I will explain later what beforeTest()
does. First the test specification:
testEmptyModel()
checks setting an empty model into the MVC, and asserts all values being null thentestInitialModel()
tests the values of the MVC as prepared by beforeTest()
from constantstestChangeValues()
tests whether values arrive in the model when I change them via controllerMind that I can not assert that some value is in the view, for example when I change the model. Reason is that the MeasurementStationView
does not provide getters and setters for its fields, and so its mock also can not provide these values.
When I want to imitate a user action, I need to call the controller's inputXxx()
listener method. Implicitly I anticipate that the view will call that controller-method when the user does that input. To be seen in line 38: controller.inputLocation(TEST_LOCATION)
, or in line 52: controller.temperatureController.inputCelsius(celsius)
.
For this to be possible hierarchically, I had to make accessible the nested controller in MeasurementStationController
, which I did by making the according field package-visible. The unit-test resides in the same package as the controller, just in a different directory, so it can see the nested controller. This is not an access-modifier hazard because the field is final
and thus immutable (needs no getter, can not have a setter).
public class MeasurementStationController implements MeasurementStationView.Listener
{
....
final TemperatureController temperatureController;
....
}
What this unit test does not assert:
It just tests model and controller. Every user gesture must be imitated by calling the according view-listener method in the controller.
Building a mock is not that difficult when you remember the Java Proxy mechanism. Teaching the mock how to behave on different calls however is difficult. And that is what we need here, because our mock must return its nested TemperatureView
hierarchy-child when view.getTemperatureView()
gets called on it.
public MeasurementStationController(MeasurementStationView<?> view) {
....
temperatureController = new TemperatureController(view.getTemperatureView());
}
Calling view.getTemperatureView()
on a mock-view would return null by default. The TemperatureController
constructor would throw a NullPointerException
then:
public TemperatureController(TemperatureView<?> view) {
....
(this.view = view).addListener(this);
}
So here is how to teach the mock the view-hierarchy:
public class MeasurementStationMcTest
{
....
@Before
public void beforeTest() throws Exception {
model = new MeasurementStationModel();
// mock the view
final MeasurementStationView<?> view = Mockito.mock(MeasurementStationView.class);
// and teach it how to behave: build together the whole view hierarchy!
final TemperatureView temperatureView = Mockito.mock(TemperatureView.class);
Mockito.when(view.getTemperatureView()).thenReturn(temperatureView);
// now build the MVC together
controller = new MeasurementStationController(view);
model.getTemperatureModel().setTemperatureInCelsius(INITIAL_CELSIUS);
model.setLocation(INITIAL_LOCATION);
controller.setModel(model);
}
....
}
After creating the model we need to create a view, because the controller requires a not-null view. The Mockito library provides static methods to create mocks, and to teach mocks how to behave. Calling Mockito.mock()
and passing the class or interface we want to mock is all we have to do. Only that the created thing is very "silly".
To teach the mock how to behave we can use the Mockito.when()
method.
Mockito.when(view.getTemperatureView()).thenReturn(temperatureView);
Read this like
"Whenview.getTemperatureView()
is called on you, please returntemperatureView
"
Don't ask me how this works, it works. It looks strange, but we can get used to it. After some time you won't think about it any more.
As soon as we have created the view mock and taught it how to behave, we can construct the controller and set a model into it.
Mock libraries need getting used to. But when we can facilitate them, we don't need to implement the mocks for our views any more. Which is a great relief!
Teaching the mock how to build the view-hierarchy surely is code duplication. Moreover when your view-hierarchy changes, the compiler would not detect that the mock builds it falsely! But mocking still is the most elegant way to write unit tests without using unstable graphical helper tools.
ɔ⃝ Fritz Ritzberger, 2017-03-08