Blog-Archiv

Dienstag, 21. Februar 2017

MVP Unit Test in Java

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).

Concept

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!

Skeleton

Here is a Java unit test skeleton that provides model, view and presenter in private fields.

 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
import org.junit.*;
import static org.junit.Assert.*;

public abstract class AbstractTemperatureMvpTest
{
    public interface ViewMock<W> extends TemperatureView<W>
    {
        /** To be called by test, simulates user input. */
        void inputCelsius(Integer newCelsius);
        
        void inputFahrenheit(Integer newFahrenheit);
        
        /** To be called by test, returns user input. */
        Integer getCelsius();

        Integer getFahrenheit();
    }
    
    /** Sub-classes must provide a mock object for the view interface. */
    protected abstract ViewMock<?> newViewMock();

    
    private final static Integer INITIAL_CELSIUS = 21;
    
    private TemperatureModel model;
    private ViewMock<?> view;
    private TemperaturePresenter presenter;
    
    @Before
    public void beforeTest() throws Exception   {
        model = new TemperatureModel();
        view = newViewMock();
        presenter = new TemperaturePresenter(view);
        
        model.setTemperatureInCelsius(INITIAL_CELSIUS);
        presenter.setModel(model);
    }
    
    ......

}

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.

Muscles

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
 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
import org.junit.*;
import static org.junit.Assert.*;

public abstract class AbstractTemperatureMvpTest
{

    ....
    
    
    @Test
    public void testEmptyModel() throws Exception   {
        presenter.setModel(model = new TemperatureModel());
        assertTemperatures(null, null);
    }

    @Test
    public void testInitialModel() throws Exception   {
        final Integer initialFahrenheit = model.toFahrenheit(INITIAL_CELSIUS);
        assertTemperatures(INITIAL_CELSIUS, initialFahrenheit);
    }

    @Test
    public void testChangeTemperatures() throws Exception   {
        assertCelsiusInput(36);
        assertFahrenheitInput(120);
    }

    
    private void assertCelsiusInput(Integer celsius) {
        final Integer fahrenheit = model.toFahrenheit(celsius);
        view.inputCelsius(celsius);
        assertTemperatures(celsius, fahrenheit);
    }

    private void assertFahrenheitInput(Integer fahrenheit) {
        final Integer celsius = model.toCelsius(fahrenheit);
        view.inputFahrenheit(fahrenheit);
        assertTemperatures(celsius, fahrenheit);
    }

    private void assertTemperatures(Integer celsius, Integer fahrenheit) {
        assertEquals(celsius, view.getCelsius());
        assertEquals(fahrenheit, view.getFahrenheit());

        assertEquals(celsius, model.getTemperatureInCelsius());
        assertEquals(fahrenheit, model.getTemperatureInFahrenheit());
    }
}

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.

Head

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
 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
import java.util.*;

public class TemperatureMvpTest extends AbstractTemperatureMvpTest
{
    @Override
    protected ViewMock<?> newViewMock() {
        return new ViewMockImpl();
    }

    private static class ViewMockImpl implements ViewMock<Object>
    {
        private Integer celsius;
        private Integer fahrenheit;
        private Collection<Listener> listeners = new ArrayList<>();
        
        @Override
        public Object getAddableComponent() {
            return "Sorry, no windowing system here!";
        }

        @Override
        public void addListener(Listener presenter) {
            listeners.add(presenter);
        }

        @Override
        public void setTemperatureInCelsius(Integer newCelsius) {
            this.celsius = newCelsius;
        }
        @Override
        public void setTemperatureInFahrenheit(Integer newFahrenheit) {
            this.fahrenheit = newFahrenheit;
        }
     
        /** To be called by test, simulates user input. */
        @Override
        public void inputCelsius(Integer newCelsius)   {
            setTemperatureInCelsius(newCelsius);
            for (Listener listener : listeners)
                listener.inputCelsius(newCelsius);
        }
        @Override
        public void inputFahrenheit(Integer newFahrenheit)   {
            setTemperatureInFahrenheit(newFahrenheit);
            for (Listener listener : listeners)
                listener.inputFahrenheit(newFahrenheit);
        }
        @Override
        public Integer getCelsius() {
            return celsius;
        }
        @Override
        public Integer getFahrenheit() {
            return fahrenheit;
        }
    }
}

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!

Frankenstein Ready

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
 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
import vaadin.examples.mvp.model2views.viewimpl.VaadinTemperatureView;
import com.vaadin.ui.Component;

public class VaadinTemperatureMvpTest extends AbstractTemperatureMvpTest
{
    @Override
    protected ViewMock<?> newViewMock() {
        return new ViewMockImpl();
    }

    private static class ViewMockImpl extends VaadinTemperatureView implements ViewMock<Component>
    {
        /** To be called by test, simulates user input. */
        @Override
        public void inputCelsius(Integer newCelsius)   {
            celsius.setValue(newCelsius);
        }
        @Override
        public void inputFahrenheit(Integer newFahrenheit)   {
            fahrenheit.setValue(newFahrenheit);
        }
        @Override
        public Integer getCelsius() {
            return celsius.getValue();
        }
        @Override
        public Integer getFahrenheit() {
            return fahrenheit.getValue();
        }
    }
}

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:

  • hierarchical MVP,
  • data binding,
  • controller refinement.



Keine Kommentare: