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.
Test Package
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 Model and Controller are tested by
"XxxMcTest".
Please find the source of the referenced MVC classes in my passed Blog articles about MVC.
Assertions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | package vaadin.examples.mvc.hierarchical; import org.junit.*; import static org.junit.Assert.assertEquals; import org.mockito.Mockito; import vaadin.examples.mvc.model2views.TemperatureView; public class MeasurementStationMcTest { private final static Integer INITIAL_CELSIUS = 21; private final static String INITIAL_LOCATION = "Lhasa"; private MeasurementStationModel model; private MeasurementStationController controller; @Before public void beforeTest() throws Exception { // TODO: assign values to model and controller fields } @Test public void testEmptyModel() throws Exception { controller.setModel(model = new MeasurementStationModel()); assertLocation(null); assertTemperatures(null, null); } @Test public void testInitialModel() throws Exception { assertLocation(INITIAL_LOCATION); final Integer initialFahrenheit = model.getTemperatureModel().toFahrenheit(INITIAL_CELSIUS); assertTemperatures(INITIAL_CELSIUS, initialFahrenheit); } @Test public void testChangeValues() throws Exception { final String TEST_LOCATION = "testLocation"; controller.inputLocation(TEST_LOCATION); assertLocation(TEST_LOCATION); assertCelsiusInput(36); assertFahrenheitInput(120); } private void assertLocation(String location) { assertEquals(location, model.getLocation()); } private void assertCelsiusInput(Integer celsius) { final Integer fahrenheit = model.getTemperatureModel().toFahrenheit(celsius); controller.temperatureController.inputCelsius(celsius); assertTemperatures(celsius, fahrenheit); } private void assertFahrenheitInput(Integer fahrenheit) { final Integer celsius = model.getTemperatureModel().toCelsius(fahrenheit); controller.temperatureController.inputFahrenheit(fahrenheit); assertTemperatures(celsius, fahrenheit); } private void assertTemperatures(Integer celsius, Integer fahrenheit) { assertEquals(celsius, model.getTemperatureModel().getTemperatureInCelsius()); assertEquals(fahrenheit, model.getTemperatureModel().getTemperatureInFahrenheit()); } } |
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 bybeforeTest()
from constantstestChangeValues()
tests whether values arrive in the model when I change them via controller
Mind 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:
- whether the model's event mechanism works
- whether the view's event mechanism works (view is mocked and thus not testable)
- whether the view shows a correct Fahrenheit temperature when the Celsius field has been changed
It just tests model and controller. Every user gesture must be imitated by calling the according view-listener method in the controller.
Building the View Mock
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.
Resume
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.
Keine Kommentare:
Kommentar veröffentlichen