Tuesday, April 21, 2009

Writing a unit testable backend

This is another post in the Schlüsselmeister series, in which I try to build a GWT/App Engine based key store application from scratch. This entry should hopefully be readable and interesting on its own, but it might still be worthwhile to check out the previous posts.

Over the next two or three posts, I will focus on iteration three of my little key store application. The previous two iterations were about implementing a GWT based client and establishing a protocol that the client could use to communicate with the server. The major emphasis in the following stage of development is getting the application fully functional (but not pretty -- I am saving stuff like CSS and a nicer landing page for the final iteration). The things I have to target are:


  • Build a backend on App Engine that handles incoming requests properly.

  • Implement a Scrambler class in GWT that encrypts data in the browser before it gets sent to the server.

  • Enable and enforce https access to protect encryption on the wire.



This post will be about the App Engine backend class, AppEngineBackendImpl. Like in the previous iterations, I will post the source for download once the stage is code complete, so I will shorten the code a little bit for brevity (like imports, or some of the methods not essential for this post). As mentioned in my alternative approach to using the data store, I am not going to make use of JPA or JDO for designing my data model. My theory is that any given key database is so small that it easily fits into a a single entity in the data store. Therefore, I am using Protocol buffers to specify my data, and a ProtocolBufferPersistence to store each entity in the store. The data model definition goes something like this:

package schluesselmeister;

option java_package = "com.appenginefan.schluesselmeister.server";
option java_outer_classname = "DataModel";
option optimize_for = SPEED;

message KeyData {
required int64 id = 1;
required string key = 2;
required string password = 3;
optional string description = 4;
optional string url = 5;
required string category_name = 6;
}

message KeyDatabase {
required int64 last_id = 1;
repeated KeyData data = 2;
}


Using the code generator application, this data model gets compiled into two new classes, KeyData and KeyDatabase. KeyDatabase -- which is the top level class I am going to persist -- is essentially a list of KeyData entries, plus a marker that remembers the last integer id being used for storing a key object. Remember, all string components are encrypted, so the id is the only piece of data the server can identify the key entries by. I had also experimented with having key data stored in a set of Category objects but decided that the added complexity in code was probably not worth the effort.

The following abreviated server class demonstrates how the backend handles a request to save new data in the Persistence:

@SuppressWarnings("serial")
public class AppEngineBackendImpl
extends RemoteServiceServlet implements Backend {

private final Persistence<KeyDatabase> store;
private final UserService userService;
private final String redirectPath;

// Constructor
// [...]

/**
* @return the email of the current user, or null if
* nobody is logged in
*/
private String getUserEmail() {
User user = userService.getCurrentUser();
if (user != null) {
return user.getEmail();
}
return null;
}

/**
* Returns commands that should be sent to the client if
* no user is logged in
*
* @return a singleton list with a properly build login
* command
*/
private List<LoginRequiredCommand> getMissingUserResponse() {
// [...]
}

/**
* @return a list prepopulated with some standard
* commands, like "access granted"
*/
private List<ServerToClientCommand> getResponseList() {
// [...]
}

/**
* Creates a command that sets a list of categories on the
* client
*
* @param database
* the database to work on or null
* @return a properly initialized SetCategoriesCommand
*/
private SetCategoriesCommand getCategoriesCommand(
KeyDatabase database) {
// [...]
}

/**
* Creates a command that updates data for a particular
* category on the client
*
* @param categoryName
* the name of the category to work on
* @param database
* the database to work on or null
* @return a properly initialized SetDataCommand
*/
private SetDataCommand getDataCommand(
String categoryName, KeyDatabase database) {
// [...]
}

@Override
public List<? extends ServerToClientCommand> requestCategories() {
// [...]
}

@Override
public List<? extends ServerToClientCommand> requestData(
String category) {
// [...]
}

@Override
public List<? extends ServerToClientCommand> save(
final PasswordData data) {

// Deny access if no user is logged in
final String user = getUserEmail();
if (user == null) {
return getMissingUserResponse();
}

// Otherwise, modify the store in a transaction
final List<String> categoriesToUpdate =
Lists.newArrayList();
KeyDatabase database =
store.mutate(user,
new Function<KeyDatabase, KeyDatabase>() {
@Override
public KeyDatabase apply(KeyDatabase database) {

// Strictly speaking, a function is no longer a function
// when it has side effects like manipulating this
// list, but I don't care ;-)
categoriesToUpdate.clear();

// Is the data currently persistent?
KeyData.Builder keyData = null;
KeyDatabase.Builder builder;
int index = 0;
boolean addNewEntity = true;
if (database != null
&& data.getId() != null) {

// if we ever get performance issues,
// this could be replaced with binary
// search,
// but we would need to order the content
// of the list
// first
final long id = data.getId();
for (KeyData candidate : database
.getDataList()) {
if (candidate.getId() == id) {
keyData =
KeyData.newBuilder(candidate);
addNewEntity = false;
break;
}
index++;
}
}

// If there is nothing at all in the
// database, start from scratch
if (database == null) {
builder = KeyDatabase.newBuilder();
builder.setLastId(0);
} else {
builder =
KeyDatabase.newBuilder(database);
}

// Do we have to create a new entry?
if (keyData == null) {
keyData = KeyData.newBuilder();
keyData.setId(builder.getLastId() + 1);
builder.setLastId(keyData.getId());
}

// Are we changing categories?
else if (!keyData.getCategoryName().equals(
data.getCategory())) {
categoriesToUpdate.add(keyData
.getCategoryName());
}

// Transfer the data
keyData.setCategoryName(data.getCategory());
keyData.setDescription(data
.getDescription());
keyData.setKey(data.getKey());
keyData.setPassword(data.getPassword());
keyData.setUrl(data.getUrl());
categoriesToUpdate.add(keyData
.getCategoryName());

// Add the data to the datastore
if (addNewEntity) {
builder.addData(keyData.build());
} else {
builder.setData(index, keyData.build());
}

// Done :-)
return builder.build();
}
});

List<ServerToClientCommand> result = getResponseList();
result.add(getCategoriesCommand(database));
for (String category : categoriesToUpdate) {
result.add(getDataCommand(category, database));
}
return result;
}

}


The save command is by far the most complex server action, because it has to make manipulations to the data model and consider a lot of different cases (new key store, new entry vs overwriting an old entry, an entry changed categories, ...). Needless to say that I did not get it right the first time: I plugged my new backend in, and everything went kaboom! New data did not appear properly on the screen; old entries got overwritten with new data. However, trying to debug those issues from the GUI would be a lot of effort. Unit tests would need to come to the rescue!

The first thing in building a unit test is finding out how to make a class testable in the first place. Do I need to set up an App Engine backend with a datastore, as described in the SDK documentation? How much plumbing code do I need?

Fortunately, this application was coded with testing in mind. One of my principle coding practices is to build my classes in an injectable fashion -- that means at a minimum that whatever external dependencies a class might have to the rest of the system, it needs to be possible to push these dependencies into the class (instead of the class fetching them by itself). For the backend, the conclusion is that it


  • should not have to control how the Persistence is instantiated

  • should not fetch the UserService itself

  • should not have to fetch any configuration details (like a redirect path for login urls) from an external configuration



AppEngineBackendImpl has a constructor that fulfills these requirements, as shown below:

  /**
* Useful constructor for unit tests, makes the backend
* injectable
*/
public AppEngineBackendImpl(
Persistence<byte[]> datastore,
final UserService service, String redirectPath) {
this.store =
new ProtocolBufferPersistence<KeyDatabase>(
datastore, KeyDatabase.getDefaultInstance());
this.userService = service;
this.redirectPath = redirectPath;
}

/**
* Standard constructor for the servlet context
*/
public AppEngineBackendImpl() {
this(new DatastorePersistence("KeyDatabase"),
UserServiceFactory.getUserService(),
"/Schluesselmeister.html");
}


Using this constructor, it is easily possible to unit test without ever having to set up a fake App Engine environment, by simply using an in-memory persistence implementation plus a mock for the UserService:

public class AppEngineBackendImplTest
extends TestCase {

private static final String USER = "user@example.com";

private Persistence<byte[]> persistence;

private UserService userService;

private String redirectPath;

private AppEngineBackendImpl backend;

private User user;

private byte[] database;

private PasswordData data;

@Override
protected void setUp() throws Exception {
super.setUp();
persistence = new MapBasedPersistence<byte[]>();
userService = EasyMock.createMock(UserService.class);
redirectPath = "/anywhere.html";
backend =
new AppEngineBackendImpl(persistence, userService,
redirectPath);

// At this point, our backend is completely set up.
// The rest is just some test data we want to use
// for our unit tests :-)
user = new User(USER, "example.com");
KeyData entry1 =
KeyData.newBuilder().setCategoryName("c1")
.setDescription("").setId(-1L).setKey("k1")
.setPassword("p").setUrl("").build();
KeyData entry2 =
KeyData.newBuilder().setCategoryName("c2")
.setDescription("").setId(-2L).setKey("k1")
.setPassword("p").setUrl("").build();
database =
KeyDatabase.newBuilder().setLastId(1).addData(
entry1).addData(entry2).build().toByteArray();
data = new PasswordData();
data.setCategory("c1");
data.setDescription("d");
data.setId(-1L);
data.setKey("k2");
data.setPassword("p3");
data.setUrl("foo");
}


With this setup, we have full control about how the environment behaves for a particular test case, without having to actually simulate logins or persisting data in a store. The following excerpt shows a relatively comprehensive list of unit tests I used to eventually get my implementation of save right. Well, at least I got it to the point where I spotted no more bugs ;-)

  private void setLoggedIn(boolean isLoggedIn) {
if (isLoggedIn) {
EasyMock.expect(userService.getCurrentUser())
.andReturn(user).once();
EasyMock.expect(
userService.createLogoutURL(redirectPath))
.andReturn("foo").anyTimes();
} else {
EasyMock.expect(userService.getCurrentUser())
.andReturn(null).once();
EasyMock.expect(
userService.createLoginURL(redirectPath))
.andReturn("foo").anyTimes();
}
EasyMock.replay(userService);
}

private void setData() {
persistence.mutate(USER, Functions.constant(database));
}

public void testSaveNotLoggedIn() {
setLoggedIn(false);
List<? extends ServerToClientCommand> response =
backend.save(data);
assertEquals(1, response.size());
assertTrue(response.get(0) instanceof LoginRequiredCommand);
}

public void testSaveNoData() {
setLoggedIn(true);
List<? extends ServerToClientCommand> response =
backend.save(data);
assertEquals(3, response.size());
assertTrue(response.get(0) instanceof AccessGrantedCommand);
assertEquals(1,
((SetCategoriesCommand) response.get(1))
.countCategories());
assertEquals(1, ((SetDataCommand) response.get(2))
.countEntities());
}

public void testOverwriteData() {
setLoggedIn(true);
setData();
List<? extends ServerToClientCommand> response =
backend.save(data);
assertEquals(3, response.size());
assertTrue(response.get(0) instanceof AccessGrantedCommand);
assertEquals(2,
((SetCategoriesCommand) response.get(1))
.countCategories());
assertEquals(1, ((SetDataCommand) response.get(2))
.countEntities());
}

public void testAddData() {
setLoggedIn(true);
setData();
data.setId(null);
List<? extends ServerToClientCommand> response =
backend.save(data);
assertEquals(3, response.size());
assertTrue(response.get(0) instanceof AccessGrantedCommand);
assertEquals(2,
((SetCategoriesCommand) response.get(1))
.countCategories());
assertEquals(2, ((SetDataCommand) response.get(2))
.countEntities());
}

public void testMoveDataToDifferentCategory() {
setLoggedIn(true);
setData();
data.setCategory("c3");
List<? extends ServerToClientCommand> response =
backend.save(data);
assertEquals(4, response.size());
assertTrue(response.get(0) instanceof AccessGrantedCommand);
assertEquals(2,
((SetCategoriesCommand) response.get(1))
.countCategories());
assertEquals(0, ((SetDataCommand) response.get(2))
.countEntities());
assertEquals(1, ((SetDataCommand) response.get(3))
.countEntities());
}

public void testDataInNewCategory() {
setLoggedIn(true);
setData();
data.setId(null);
data.setCategory("c3");
List<? extends ServerToClientCommand> response =
backend.save(data);
assertEquals(3, response.size());
assertTrue(response.get(0) instanceof AccessGrantedCommand);
assertEquals(3,
((SetCategoriesCommand) response.get(1))
.countCategories());
assertEquals(1, ((SetDataCommand) response.get(2))
.countEntities());
}


That's it for today. Feel free to post questions and comments to this blog post :-)

0 comments: