PS: Since this iteration is relatively long, I decided to split it up in two separate posts.
Today, I am going to start building a simple servlet-based backend for my key-manager application. The backend will be in-memory for now, without a lot of App Engine specific stuff in it (that would be iteration 3).
While iteration 1 was mostly about getting my project set up and plugging some widgets together, this iteration will contain some actual "business logic". I will define how the application will interact with the user, and how the user interactions will be communicated to the backend. For me, this means a couple of changes in the way I program:
- Unlike the previous stage, I can no longer simply fire up the UI and see if everything looks fine. If I introduce bugs in the code, chances are they will not be entirely obvious to me initially. They might take several steps in the UI to produce and might be tedious to repeat.
- Besides designing my code for testability, I want to make sure that I isolate the user interface as much as I can from any logic in the UI. I might decide to completely redo the layout of the frontend later on (replace my tree elements with a table, or change the "Set Scambler" button towards a text field that is always on the screen). If I choose so, I do not want to have to change (and thus retest) anything in the execution logic itself, just because I replaced a button with a textfield someplace.
For these reasons, I decide to split my UI into two layers, both expressend by interfaces: a
Display represents everything that is visualized in the browser, while a Controller manages communication to the server and reacts to whatever events may happen in the UI. Here is the definition of those interfaces:package com.appenginefan.schluesselmeister.client;
import java.util.List;
/**
* Represents the user interface. The Display contains all the code needed to
* visualize the content to the end user, but nothing else. Any more complex
* logic should be isolated in a separate class that is easier to unit-test.
*/
public interface Display {
/**
* The Controller interface represents callback methods that the display can
* use notify the non-display parts of the system about signifcant UI events.
* All UI events are of return type "void", since they are expected to be
* handled asynchronously, whithout holding up anything happening in the UI.
*/
public static interface Controller {
/**
* Notifies the backend that the user has chosen to request a different
* scrambler.
*/
public void switchScrambler(String secret);
/**
* Represents the creation of a new PasswordData object or the modification
* of an existing one.
*/
public void saveData(PasswordData data);
/**
* Notifies the controller that the user has selected a particular category.
* Passing in null means that no category is selected.
*/
public void selectCategory(String categoryOrNull);
/**
* Notifies the controller that everything is initialized
* and it can get to work.
*/
public void start();
}
/**
* Sets the controller that reacts to events in the UI. The controller must be
* set before the UI is displayed -- otherwise, bad things may happen.
*/
public void setController(Controller controller);
/**
* This method will "lock up" the UI and make it unusable. This is the state
* the UI should be in when the application is initialized (in other words, no
* connection to the server has been established, and the user should not be
* able to interact with the display).
*
* @param message
* the message that should be displayed while the UI is locked.
* Setting null as message will unlock the UI
*/
public void lockup(String messageOrNull);
/**
* Tells the display that a user is currently logged out and provides a url
* that can be used for logging in.
*
* @param url
*/
public void setLoginUrl(String url);
/**
* Tells the display that the user is currently logged in and provides a url
* that can be used for logging out.
*/
public void setLogoutUrl(String url);
/**
* Sets the list of categories that should be displayed, in the order they
* should be displayed.
*/
public void setCategories(List<String> categories);
/**
* Sets the list of entries from the password database that should be
* displayed. Content of the list is a "suggested" order, but the display may
* choose to ignore that (due to table sorting, for example)
*/
public void setPasswordData(List<PasswordData> data);
}
So, how does that help making things more testable? Let's take a look at a very simple controller-implementation that simulates a fully functional backend, but actually lives in the Javascript code on the browser:
package com.appenginefan.schluesselmeister.client;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A simple controller-implementation that does not persist anything
* but stores things in the javascript client instead.
*/
public class NaiiveDemoController implements Display.Controller {
private final Display display;
private final Map<Long, PasswordData> allData;
private long counter = 0;
private String category;
private void updateDisplay() {
List<PasswordData> dataToDisplay = new ArrayList<PasswordData>();
Set<String> categories = new HashSet<String>();
for (PasswordData data : allData.values()) {
categories.add(data.getCategory());
if (data.getCategory().equals(category)) {
dataToDisplay.add(data);
}
}
List<String> sorted = new ArrayList<String>(categories);
Collections.sort(sorted);
display.setCategories(sorted);
display.setPasswordData(dataToDisplay);
}
public NaiiveDemoController(Display display) {
this(display, new HashMap<Long, PasswordData>());
}
public NaiiveDemoController(
Display display, Map<Long, PasswordData> dataContainer) {
this.display = display;
this.allData = dataContainer;
}
@Override
public void saveData(PasswordData data) {
data = data.clone();
if (data.getId() == null) {
data.setId(++counter);
}
allData.put(data.getId(), data);
updateDisplay();
}
@Override
public void selectCategory(String categoryOrNull) {
this.category = categoryOrNull;
updateDisplay();
}
@Override
public void switchScrambler(String secret) {
// do nothing
}
@Override
public void start() {
updateDisplay();
display.setLogoutUrl("http://www.google.com");
display.lockup(null);
}
}
Code like this is usually throwaway, and I would not use it for anything beyond testing if the Javascript UI actually works. However, for the sake of demonstration, let's try to test this controller out and make sure that it behaves the way I intend it to. We would like to make sure that the following things happen:
- Upon start of the controller, the display should get unfrozen and present itself as "logged in" (with the logout URL being set to www.google.com).
- Selecting a category will display the data in that category.
- Modifying data for the current category will update the displayed data.
- Modifying data that contains a yet unknown category will update the list of categories.
For a real world app, there would probably be more test cases, but this should be enough for the scope of this post. So, let's get to work on those tests! The tools I am going to use are JUnit (a Java unittesting framework) and EasyMock (a framework that can help me by creating mock
Display objects for my test). Using these tools, I will create a simple test class that can be used to verify any of the test cases mentioned above:package com.appenginefan.schluesselmeister.tests;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.easymock.EasyMock;
import com.appenginefan.schluesselmeister.client.Display;
import com.appenginefan.schluesselmeister.client.NaiiveDemoController;
import com.appenginefan.schluesselmeister.client.PasswordData;
import junit.framework.TestCase;
public class NaiiveDemoControllerTest extends TestCase {
private NaiiveDemoController controller;
private Display display;
private Map<Long, PasswordData> data;
/**
* Set up this test by creating a controller with a mock display
* and a map that this unit test can manipulate.
*/
@Override
protected void setUp() throws Exception {
super.setUp();
data = new HashMap<Long, PasswordData>();
display = EasyMock.createMock(Display.class);
controller = new NaiiveDemoController(display, data);
}
}
Our test class contains three private fields:
- the controller we would like to test,
- a
Displayobject that the controller is connected to, and - the data structure that the controller uses to store its internal password data. When we create our
NaiiveDemoController, we inject thatMapinto the constructor, so that our unit tests have the possibility to get its internal data store into whatever state we need for a particular test.
Before I continue with this, I would like to point out: although we use sophisticated Java tools (JMock creates mock Displays for us -- how cool is that?), we are actually testing features that are going to run as Javascript on the web browser. If you have ever needed to use a tool like Firebug to dig through Javascript code to trace down an issue with a client, you will appreciate that we can use the full power of Eclipse here and write our unit tests in the Java language! But enough of this, let's look at the first of our unit tests:
/**
* Upon start of the controller, the display should get unfrozen
* and present itself as logged in.
*/
public void testStart() {
// Record what should happen to the display
// when controller.start() is called:
// the list of categories should be emptied
display.setCategories(new ArrayList<String>());
// the table of password data should be emptied
display.setPasswordData(new ArrayList<PasswordData>());
// a logout url should be set
display.setLogoutUrl("http://www.google.com");
// the display should be unlocked
display.lockup(null);
// We're done recording our expectations, so let's
// switch into replay mode and simply try it out
EasyMock.replay(display);
controller.start();
// Did out controller call all the methods with the
// right parameters?
EasyMock.verify(display);
}
What exactly have we done? The first half of our test was the recording stage, where we told EasyMock what we expect to happen to our fake display. If the methods would not have been of
void return type, we could have also recorded what our fake should return. At the end of the recording stage, we put our mock into replay mode and call the method on the controller that we would like to test (in this case, controller.start(). Last but not least, we call EasyMock.verify to make sure that all the calls to the display .Let's try another unit test. This time, we will put some data into our
Map and use it to predict what is going to happen to our mock Display: /**
* Selecting a category will display the data in that category.
*/
public void testSelectCategory() {
// Put some data into the map
PasswordData category1 = new PasswordData();
category1.setId(-1L);
category1.setCategory("1");
data.put(-1L, category1);
PasswordData category2 = new PasswordData();
category2.setId(-2L);
category2.setCategory("2");
data.put(-2L, category2);
// What should happen when category 1 gets selected?
display.setCategories(Arrays.asList("1", "2"));
display.setPasswordData(Arrays.asList(category1));
// Let's try it out
EasyMock.replay(display);
controller.selectCategory("1");
EasyMock.verify(display);
}
That was fun, wasn't it? Let's do another of our sample tests. In this example, the replay data in our mock
Display does not test everything I'd like to verify, so I add some manual verification code of my own: /**
* Modifying data for the current category will update
* the displayed data.
*/
public void testUpdateData() {
// Put some data into the map
PasswordData original = new PasswordData();
original.setId(-1L);
original.setCategory("1");
data.put(-1L, original);
// First, we have to select the category, so expect some
// updates to the display for that
display.setCategories(Arrays.asList("1"));
display.setPasswordData(Arrays.asList(original));
// Create a modified version of the data
PasswordData modified = original.clone();
modified.setDescription("changed");
// After the data gets saved, setCategories and
// setPasswordData get called a second time
display.setCategories(Arrays.asList("1"));
display.setPasswordData(Arrays.asList(modified));
// Let's try it out
EasyMock.replay(display);
controller.selectCategory("1");
controller.saveData(modified);
EasyMock.verify(display);
// PasswordData's equals method is overloaded to only compare
// the ID field, so original.equals(modified) in this case.
// To make sure that our Map was actually modified, let's get
// the changed description out of there and take a look
assertEquals("changed", data.get(-1L).getDescription());
}
Only one test remaining, I am afraid. This particular test does not have anything new and exciting in it; I am mostly posting it for completeness.
/**
* Modifying data that contains a category not known
* yet will update the list of categories.
*/
public void testNewCategory() {
// Create some data with an unknown category
PasswordData data = new PasswordData();
data.setId(-1L);
data.setCategory("new");
// When the data gets saved, the new category should
// be sent to the display. Since no category is selected,
// the list of data should remain empty
display.setCategories(Arrays.asList("new"));
display.setPasswordData(new ArrayList<PasswordData>());
// Let's try it out
EasyMock.replay(display);
controller.saveData(data);
EasyMock.verify(display);
}
In summary
By separating the UI setup code from the controller code, I was able to isolate the actual UI frontend code from any processing logic that went beyond connecting a button with an action listener. Since the
Controller did not contain any UI frontend code, it was easily unit-testable using standard frameworks like JUnit and EasyMock. Testing as much of the GWT code in Java increases the overall quality of the resulting software and makes it easier to track, reproduce and eliminate defects in the long run.By the way, you can check out the code created in this post by clicking here.
0 comments:
Post a Comment