Donnerstag, 3. Januar 2013

ui-less Test Driven User Interface Development

Whenever you start a project that deals with technical problems, you are forced to write a simple application to show the basic concepts. Since I am going to demonstrate a concept I will save most technical details for a later post.
My topic today is TDD (test driven development). Instead of writing lots of code and check if everything works by re-running the application you write a test for each feature. Unit testing is a state of art technique to ensure quality of code. But for most user interfaces, writing test is a really cumbersome work. Regularly a specific test framework, that starts your application and simulates the user interaction, is needed. This type of test is really useful for a thorough quality assurence. But to add new features quickly to your user interface thats a real costly in terms of labor.

Example

Lets get started by very simple UI component. You have one input field where you enter your commands and a large text area where you see what you type with additional information by some service.


The upper part, with the friendly "welcome" text is our protocol text area while the lower part is the input area.

Now the first step is, that we do some meaningful specification. Like, whenever I type a text into the input area and press the CRT Key, I want that some more text is in the protocol than before.

Let us create a unit test that does exactly that:

public class MainFrameTest {

    @Test
    public void pressingTheCrtKeySendsTheInputcontentToTheProtocol() {
        int initialLength = view.getProtocol().getValue().getLength();
        enterCommand("ein Text");
        assertThat(view.getProtocol().getValue().getLength(), greaterThan(initialLength));
    }
}

Of course this code, will not compile (yet). Since we test-drive there is no "view" yet nor a class for that view. The Method enterCommand is technical and yet unknown. Lets first write the enterCommand, and thus define what the view needs to provide:

    // -- helper
    private void enterCommand(String inputText) {
        setInputText(inputText);
        view.getInputEvent().sendEvent(HasKeyEvent.KeyEvent.Type.KEY_UP, 0, java.awt.event.KeyEvent.VK_ENTER);
    }
    private void setInputText(String inputText) {
        view.getInputValue().setValue(inputText);
    }


Currently the view requires these methods:
  • getProtocol
  • getInputValue
  • getInputEvent
Knowing that we can define an interface for our user-interface to reflect upon that fact:

package org.rosuda.ui.main;

import javax.swing.text.html.HTMLDocument;
import org.rosuda.ui.core.mvc.HasKeyEvent;
import org.rosuda.ui.core.mvc.HasValue;
import org.rosuda.ui.core.mvc.MVP;

public interface MainView<C> extends MVP.View<C> {
    HasValue<String> getInputValue();
    HasValue<HTMLDocument> getProtocol();
    HasKeyEvent getInputEvent();
}


Defining a test has provided us with a java interface, instead of a bloated AWTSwing or other user interface technique. You might ask, will this ever work ? I assure, stay with me, and I'll convince you.

To make this test go green and creating functional java code we will need some more java classes. Unless you are familiar with the MVP (Model View Presenter) pattern, which deserves an own post you might be confused how to write code for making your test go green.

Using j.o.r.i.s MVP and MVP Test support you can inherit from the base class MVPTest.
This MVPTest.java requires the MVP Components as generic arguments and an aditional Model-initialisation test helper class.

The signature looks overwhelming:
public abstract class MVPTest<MODEL, VIEW, PRESENTER extends MVP.Presenter<MODEL, VIEW>, MODELINITIALIZER extends ModelInitializer<MODEL>>
Since I want to cover MVP in an later post, let's write the code that is required to come back to green:
  • first we use the abstract class MVPTest for our unit Test:
public class MainFrameTest extends MVPTest<MainModel, MainView<Void>, MainPresenter<Void>, MainFrameTestModelData> { @Override protected MainView<Void> createTestViewInstance() { return new MainFrameTestView(); } }
  • an implementation for the model initialization, currently we do not need special model values so this is an empty implementation
public class MainFrameTestModelData extends ModelInitializer{

    @Override
    protected void initModel(MainModel model) {
    }

}
  • we need a mock UI for test wiring:
public class MainFrameTestView extends DefaultTestView implements MainView {

    private HasValue input = new DefaultHasValue();
    private HasValue protocol = new DefaultHasValue();
    private HasKeyEvent inputEvent = new DefaultHasKeyEvent();

    @Override
    public HasValue getInputValue() {
 return input;
    }

    @Override
    public HasValue getProtocol() {
 return protocol;
    }

    @Override
    public HasKeyEvent getInputEvent() {
 return inputEvent;
    }

}
  • we have to create the MVP classes according to the j.o.r.i.s framework:
public class MainModel implements MVP.Model {

    private final HTMLDocument protocol;

    public MainModel() throws IOException {
 protocol = new HTMLDocument();
 protocol.setParser(new ParserDelegator());
 BufferedReader htmlStream = null;
 try {
     htmlStream = new BufferedReader(new InputStreamReader(MainFrame.class.getResourceAsStream("/gui/html/welcome.html")));
     EditorKit kit = getEditorKit();
     kit.read(htmlStream, protocol, 0);
 } catch (Exception e) {
     throw new RuntimeException(e);
 } finally {
     if (htmlStream != null) {
  htmlStream.close();
     }
 }
    }

    private EditorKit getEditorKit() {
 return new HTMLEditorKit();
    }

    HTMLDocument getProtocol() {
 return protocol;
    }
    
}
public class MainPresenter implements MVP.Presenter> {

    @Override
    public void bind(final MainModel model, final MainView<C> view, final MessageBus messageBus) {

    public void unbind(final MainModel model, final MainView<C> view, final MessageBus messageBus) {

    }
}
That's all to get the test running. We're not green yet, but this suffices to compile the test.

Back to green

We've finally covered all required classes. A model class, holding the protocol data, a mock view object and a controller class, the MainPresenter.
Since the model and the view have no knowledge of each other yet we need to implement the bind(..) method in the presenter and we are done:

    @Override
    public void bind(final MainModel model, final MainView<C> view, final MessageBus messageBus) {
 view.getProtocol().setValue(model.getProtocol());
 view.getInputEvent().addKeyEventListener(new HasKeyEvent.KeyListener() {
     @Override
     public void onKeyEvent(HasKeyEvent.KeyEvent event) {
  if (HasKeyEvent.KeyEvent.Type.KEY_UP.equals(event.getType()) && KeyEvent.VK_ENTER == event.getKeyCode()) {
      final String currentValue = view.getInputValue().getValue();
      appendHTML(model, new StringBuilder("<div class=\"command\">&gt; ").append("<a href=\"").append(StringEscapeUtils.escapeHtml(currentValue))
       .append("\">").append(currentValue).append("</a>").append("</div>").toString());
      view.getInputValue().setValue("");
      messageBus.fireEvent(new CRTKeyEvent(currentValue));
  }
  
     }
 });
    }

    private void appendHTML(final MainModel model, final String htmlText) {
 final HTMLDocument targetDoc = model.getProtocol();
 final Element body = targetDoc.getElement("htmlbody");
 final Element lastChild = body.getElement(body.getElementCount() - 1);
 try {
     targetDoc.insertAfterEnd(lastChild, htmlText);
 } catch (final Exception e) {
     LOG.error(e);
 }
    }

Conclusions

Making use of the MVP Pattern we deal with a java interface for the UI. Thus we can test wiring code and logic without dealing with real UI - code. 
By following the test driven paradigma we could implement a view linked to the model. The features are defined in the test. This allows much faster test-driving without neglecting any feature test, or delay testing for later.
Apologies for the non-optimal code format. For better readability try any java GUI and download the sources.



Keine Kommentare:

Kommentar veröffentlichen