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:
@Test
public void pressingTheCrtKeySendsTheInputcontentToTheProtocol() {
int initialLength = view.getProtocol().getValue().getLength();
enterCommand("ein Text");
assertThat(view.getProtocol().getValue().getLength(), greaterThan(initialLength));
}
}
// -- helper
private void enterCommand(String inputText) {
setInputText(inputText);
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:
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);
}
view.getInputValue().setValue(inputText);
}
Currently the view requires these methods:
- getProtocol
- getInputValue
- getInputEvent
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();
}
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();
}
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:
- 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 MainPresenterimplements 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:
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\">> ").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