Layers of abstraction in Unit Tests: Have an Implementation layer

Summary

Differing levels of abstraction can aid or hinder the task of maintaining unit tests.

Disclaimer

This idea is still a work in progress, the product of working on one set of tests on one project but even if this just helps people look at JUnit tests from a fresh angle my time writing this post will have been worthwhile (I tell myself).

Problem

JUnit tests in Java are normal classes which either extend TestCase (JUnit 3) or are annotated as tests (JUnit 4). As they are Java classes, this allows us to use inheritance and to abstract out some or all of the details. Too little abstraction in our test cases can result in a test case where it is almost impossible to see what is being set up and what is being verified, there are lots of lines of code and these will typically need to be commented to show which block starts where. Too much abstraction can lead to the same problem but due to a lack of structure in the class at hand.

In a nutshell my requirements story is this:

As a developer maintaining Unit Tests, I want to see in code the prerequisites, the method invocation and verifications, with a layer of abstraction that hides low-level concerns but contains enough of the domain to see what is happening.

Too much detail:

public class LoginTest {

  @Test
  public void testLogin() {
    // Set up login request, mock authenticator and * inject into the LoginService.
    final LoginRequest loginRequest = new LoginRequest();
    final String username = “testuser”;
    final String password = “testpass”;
    loginRequest.setUsername(username);
    loginRequest.setPassword(password);
    MockAuthenticator mockAuthenticator = new MockAuthenticator();
    mockAuthenticator.setValidCredentials(username, password);
    loginService.setAuthenticator(mockAuthenticator);

    // Call login method being tested:
    final LoginResult result = loginService.login(loginRequest);

    // Verify the result 
    Assert.assertTrue(result.isSuccessful);
    }
}

Not enough detail:

public class LoginTest extracts AbstractSuccessfulLoginServiceTest {
  // Sadly, there are test classes like this and they will fail
  protected LoginRequest createRequest() {
    return new SuccessfulLoginRequest();
  }
}

Solution

Make the test class copy the story or script and push everything else into an implementation layer. This is akin to the Fixture classes sometimes used, the more this is a complete layer supporting the TestCase classes the better, it is very easy for supporting classes to become disjointed and hard to implement a coherent strategy across the board.


public class LoginTest {
  @Test
  public void testLogin() {
    // Make the login service use a mock authenticator which generates a valid username/password for us.
    MockAuthenticator mock = new MockAuthenticator();
    final LoginRequest request = mock.createGoodRequest();
    loginService.setAuthenticator(mock);

    // Call login method being tested:
    final LoginResult result = loginService.login(request);

    // Verify the result
    Assert.assertTrue(result.isSuccessful);
    }
}

Notes

If our unit test classes are just normal classes and we are trying to write good code – remove duplication, etc – why are we not abstracting out to the nth degree? We/I would do this with non-test code. The difference is that each unit test is an interface to the system. Where we run a system/application as its own collection of components, with a unit test each test is a discrete entity with its own functional requirements. At least sometimes, we can’t/shouldn’t allow too much interdependence between these classes. They have to be able to stand on their own. The implementation of what they wish to be done, creating datasets, setting up mocks etc, this can be abstracted out but a top level dictating what it is to abstract out, that should remain.

DSLs

Test classes are a great place to use a Domain Specific Language. Where a number of tests rely on a rich layer to setup or verify tests this can be successfully aided by a DSL-like structure, with lines like the following:


Verify.that(loginResult).isSuccessful();
Verify.that(outputFile).contains(domainModels);

We are now working with the problem domain in a language specific to the problem. For more information on DSLs please see
this article.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s