Testing Recipes, Part 1 – Self-service

Should the testing frameworks or methodologies used, influence the design of the implementation code? I’m sure most would respond in the negative, but sometimes this is very hard to avoid. I have been attempting to overcome some of this influence by finding ways to test code that I had previously refactored to make it more ‘testable’. To achieve this, I have been building on use of the standard Mockito syntax, to apply the extensions provided by PowerMock (with Mockito). This first example uses just Mockito.

The first problem I will address is how to test services that use their own services (‘self-service’). This scenario is best explained by the code below showing a ‘client’ service that allows the service-user to call methods on the client.

public class Client {

	public void create(){
		//perform action
	}

	public void delete(){
		//perform action
	}

	public void recreate(){
		delete();
		create();
	}

}

Three methods are available: create(), delete() and recreate(). Each method performs an action on the client, but recreate() is different because the action performed makes use of other service actions. More complex scenarios could be presented where a method would call other service methods multiple times, or vary calls according to business logic, but this simple example is suitable to explain a typical use-case.

The testing problem comes when wanting to test the recreate() method. Let’s assume that our client makes calls to a ‘databaseService’ to perform the underlying actions.

Approach 1 – Test the underlying interactions

A simple approach would be as follows:

	@Test
	public void recreateDeletesThenCreates() {
		client.recreate();

		//verify underlying actions
		verify(databaseService).runSQL('DELETE...');
		verify(databaseService).runSQL('INSERT...');
	}

The problem, I feel, with this test, is that we are not testing the functionality of the recreate() method, but instead we are treating the Client as a black box and testing how the client interacts with the underlying service. This would be the correct approach for the simpler create() and delete() methods but we are now open to duplication in our test suite. If the lower level methods become more complex, then tests for the higher level methods will increasingly duplicate already tested functionality. This problem begins immediately because the line

verify(databaseService).runSQL('DELETE...');

will presumably already be present in the test for delete(). If we want to refactor delete() then we have to change two or more tests.

Approach 2 – Extract an ‘extended’ service

Alternatively we can extract an ‘extended’ service which provides only the functionality that is defined in terms of the basic service methods:

public class BasicClient {

	public void create(){
		//perform action
	}

	public void delete(){
		//perform action
	}

}

public class ExtendedClient {

	private BasicClient client;

	public void recreate(){
		client.delete();
		client.create();
	}

}

We can then test the recreate() in terms of interactions with the BasicClient (the setInternalState() method is part of the Mockito Whitebox class):

public class ExtendedClientTest {

	@Mock BasicClient basicClient;

	ExtendedClient extendedClient;

	@Before
	public void setUp(){
		MockitoAnnotations.initMocks(this);

		extendedClient = new ExtendedClient();
		setInternalState(extendedClient, "client", basicClient);
	}

	@Test
	public void recreateCallsDeleteAndCreateOnTheBasicClient() {
		extendedClient.recreate();

		verify(basicClient).delete();
		verify(basicClient).create();
	}

}

The problem I see here is that we have let our testing needs influence our implementation. The approach of extracting services to reduce complexity is often the correct approach to writing more testable code. However, if no concept is reflected in the domain and the extended service is extracted purely for the purposes of testing the higher level methods, then extracting the service only adds complexity, and should therefore be avoided.

Approach 3 – Verify interaction between methods using spy

My suggested approach is to take advantage of Mockito’s spy() method. Creating a spy of the Client creates an object that can be treated like a mock (stubbing and method verification is possible), but maintains the original implemented code until stubbed.

public class ClientTest {

	Client client;
	
	@Before
	public void setUp(){
		client = spy(new Client());		
	}
	
	@Test
	public void recreateCallsDeleteThenCreate() {
		client.recreate();
		
		verify(client).delete();
		verify(client).create();
	}
	
}

We now have a Client that cleanly represents the service we wish to offer, but at the same time we can test method in terms of their interactions – testing methods in this way is generally best avoided but our Client presents a counter-example. Using this approach we must be careful to avoid the common pitfall of testing the implementation rather than the behaviour. I feel however that this approach allows us to build up interactions between methods inside a class without resulting in an explosion of test complexity.

Even the comments for the spy() method itself warn against it’s usage:

Real spies should be used carefully and occasionally, for example when dealing with legacy code.

and in general I concur, but the example presented above represents a scenario where attempting to avoid using this feature will comprise the design of the system.

These techniques are entirely compatible with a test-driven approach and provide a much cleaner way of adding higher level methods to services that already provide a set of lower level methods. I intend to explore testing internal private utility methods later in this series which is also closely related to this problem.

Thanks to Johan for recommending improvements to the code examples.

Advertisements