In my last post I promised to share some tips that can help developers write effective integration tests.

Committing to a database is expensive. In many standard SQL databases, it’s pretty much the most expensive thing you can do. By comparison, querying is relatively inexpensive. If possible, you should structure your integration tests in such a way that you assert as much as possible for each commit that’s made.

Consider this test. Rally supports the notion of a hierarchy of projects, structured into a tree. Individual users can have access to any node(s) within the tree and should only see data from those projects. This test creates users and projects and verifies our permission model.

public void whenOnParentScopedDownOnlyIncludeChildProjectsWhereUserHasAccess(PersistenceContext context) {
Project parent = createProject();
Project editorAccessChild = createProject(parent);
Project viewerAccessChild = createProject(editorAccessChild);
Project noAccessChild = createProject(parent);

User user = createUserAsProjectEditor(parent);
makeProjectEditor(user, editorAccessChild);
makeProjectViewer(user, viewerAccessChild);
context.commitAndReset();

ProjectOidsInScope scope = scopeTo(parent).as(user).includeChildren();

assertThat(scope.get(), containsOidsOf(parent, editorAccessChild, viewerAccessChild));
}

public void whenOnChildScopedUpOnlyIncludeParentProjectsWhereUserHasAccess(PersistenceContext context) {
Project noAccessGrandParent = createProject();
Project viewerParent = createProject(noAccessGrandParent);
Project editorProject = createProject(viewerParent);

User user = createUserAsProjectEditor(editorProject);
makeProjectViewer(user, viewerParent);
context.commitAndReset();

ProjectOidsInScope scope = scopeTo(editorProject).as(user).includeParents();

assertThat(scope.get(), containsOidsOf(editorProject, viewerParent));
}

These are good tests. With a little understanding of the Rally domain, they’re highly readable and they test the right things.

So what’s the problem? We’re committing twice when once will do. While the impact on any individual test is fairly minimal, the impact of unnecessary commits is compounded when spread across thousands of tests. Even within this test class, these aren’t the only two tests. There are ten.

What if we wrote the test like this?

private Project editorProject;
private Project editorAccessChild;
private Project viewerAccessChild;
private Project noAccessChild;
private User user;
private Project noAccessGrandParent;
private Project viewerParent;

@BeforeMethod
protected void setupData(PersistenceContext context) {
if(dataCreated) return;
dataCreated = true;
noAccessGrandParent = createProject();
viewerParent = createProject(noAccessGrandParent);
editorProject = createProject(viewerParent);
editorAccessChild = createProject(editorProject);
viewerAccessChild = createProject(editorAccessChild);
noAccessChild = createProject(editorProject);

user = createUserAsProjectEditor(editorProject);

makeProjectViewer(user, viewerParent);
makeProjectEditor(user, editorAccessChild);
makeProjectViewer(user, viewerAccessChild);

context.commitAndReset();
}

public void whenOnParentScopedDownOnlyIncludeChildProjectsWhereUserHasAccess(PersistenceContext context) {
ProjectOidsInScope scope = scopeTo(editorProject).as(user).includeChildren();

assertThat(scope.get(), containsOidsOf(editorProject, editorAccessChild, viewerAccessChild));
}

public void whenOnChildScopedUpOnlyIncludeParentProjectsWhereUserHasAccess(PersistenceContext context) {
ProjectOidsInScope scope = scopeTo(editorProject).as(user).includeParents();

assertThat(scope.get(), containsOidsOf(editorProject, viewerParent));
}

We’ve moved the data creation into a setup method that runs (and commits) only once. We’ve sacrificed a small amount of readability within each test because the data setup isn’t directly adjacent to the actions and assertions. We have, however, gained some consistency across tests. Once we know the data structure we’re working with, we don’t need to mentally parse each test separately to figure out what shape the data is in before reading the rest of the test.

Integration tests are expensive, so achieving maximum bang for buck is important. If you can verify multiple behaviors while only doing the expensive part of the test once, DO IT!