Context
- In a TDD environment, the same coding standards should be enforced on Unit Test code as on application code. Often, however this isn’t the case! Remember: unit tests can contain technical debt too!
- Mocking service calls with EasyMock involves repetitive code.
- This repetitive code can be simplified, and rendered more fluent with the Builder pattern.
Benefits of a MockBuilder
- Sharpened focus: unit tests becomes more focussed on the code under test , rather than code written to support or prepare the test.
- Cheaper to write more and more tests.
First Iteration: mocking a service call to the ClientService with EasyMock
Consider an example of a unit test where there is a call to the ClientService that we want to replace with a call to a Mock:
@Test public void testLoadPresetFormWithClient() throws RemoteException { NewBusinessApplication nba = NewBusinessApplicationBuilder.create() .withPresetOwner() .build(); nba.getOwner().setRegisteredAddress(null); nba.getOwner().setClientNumber("12345678"); Client stubbedClient = new Client(); stubbedClient.setAddress(new Address()); stubbedClient.getAddress().setLine1("Client Address 1"); stubbedClient.getAddress().setLine2("Client Address 2"); stubbedClient.getAddress().setLine3("Client Address 3"); stubbedClient.getAddress().setLine4("Client Address 4"); stubbedClient.getAddress().setLine5("Client Address 5"); ClientService mockClientService = EasyMock.createMock(ClientService.class); EasyMock.expect(mockClientService.findByClientNumber((String)EasyMock.anyObject())).andReturn(stubbedClient).anyTimes(); EasyMock.replay(mockClientService); EasyMock.verify(mockClientService); ClientServiceDelegate.getInstance().setClientService(mockClientService); EmployerCorrespondenceDetailsForm form = service.loadPresetForm(nba); assertFormAndClientRegisteredAddressEquals(form,mockClientService.findByClientNumber("12345678")); assertFormAndApplicationBusinessAddressEquals(form,nba); }
The vast majority of the code in this unit test is support code, written to mock the client service call, and ensure a stubbed client is used for the purposes of the test.
In fact , only the last three lines of this test method are actual test code, executing the method under test, and evaluating the results
Second Iteration: Create ClientServiceMockBuilder
All the semi-boilerplate EasyMock code related to mocking the client service is now moved to it’s own object, where the builder pattern is implemented:
public class ClientServiceMockBuilder { private ClientService mockClientService; public static ClientServiceMockBuilder create() { return new ClientServiceMockBuilder(); } public ClientService build() { EasyMock.replay(mockClientService); EasyMock.verify(mockClientService); return mockClientService; } private ClientServiceMockBuilder() { mockClientService = EasyMock.createMock(ClientService.class); } public ClientServiceMockBuilder withFindByClientNumberStubbedClient(){ try{ EasyMock.expect(mockClientService.findByClientNumber((String)EasyMock.anyObject())).andReturn(createStubbedClient()).anyTimes(); } catch(RemoteException e){ throw new RuntimeException(e); } return this; } private Client createStubbedClient(){ Client client = new Client(); client.setAddress(new Address()); client.getAddress().setLine1("Client Address 1"); client.getAddress().setLine2("Client Address 2"); client.getAddress().setLine3("Client Address 3"); client.getAddress().setLine4("Client Address 4"); client.getAddress().setLine5("Client Address 5"); return client; } }
Revisiting the Unit Test, it is now a lot cleaner, and more readable. More to the point though, we are pointing the way to any unit test that needs to mock a call to the Client Service:
@Test public void testLoadPresetForm_WithClient() throws RemoteException { NewBusinessApplication nba = NewBusinessApplicationBuilder.create() .withPresetOwner() .build(); nba.getOwner().setRegisteredAddress(null); nba.getOwner().setClientNumber("12345678"); ClientService mockClientService = ClientServiceMockBuilder.create() .withFindByClientNumberStubbedClient() .build(); ClientServiceDelegate.getInstance().setClientService(mockClientService); EmployerCorrespondenceDetailsForm form = service.loadPresetForm(nba); assertFormAndClientRegisteredAddressEquals(form,mockClientService.findByClientNumber("12345678")); assertFormAndApplicationBusinessAddressEquals(form,nba); }
Third Iteration: Abstract out common MockBuilder behaviour
If your unit test code involves the use of many different mocked objects, you’ll be building a lot of MockBuilder objects to correspond with these. You’ll also quickly notice repetition of certain code, which can be abstracted to an abstract parent class called MockBuilder, as below:
public abstract class MockBuilder<T extends Object> { protected T mockedObject; public T build() { EasyMock.replay(mockedObject); EasyMock.verify(mockedObject); return mockedObject; } }
The updated ClientServiceMockBuilder now looks like this:
public class ClientServiceMockBuilder extends MockBuilder<ClientService> { public static ClientServiceMockBuilder create() { return new ClientServiceMockBuilder(); } private ClientServiceMockBuilder() { mockedObject = EasyMock.createMock(ClientService.class); } public ClientServiceMockBuilder withFindByClientNumberStubbedClient(){ try{ EasyMock.expect(mockedObject.findByClientNumber((String)EasyMock.anyObject())).andReturn(createStubbedClient()).anyTimes(); } catch(RemoteException e){ throw new RuntimeException(e); } return this; } private Client createStubbedClient(){ Client client = new Client(); client.setAddress(new Address()); client.getAddress().setLine1("Client Address 1"); client.getAddress().setLine2("Client Address 2"); client.getAddress().setLine3("Client Address 3"); client.getAddress().setLine4("Client Address 4"); client.getAddress().setLine5("Client Address 5"); return client; } }
And we can extend the MockBuilder again as required. For example, here’s another implementation, a Spring MVC BindingResultMockBuilder
public class BindingResultMockBuilder extends MockBuilder<BindingResult> { public BindingResultMockBuilder() { mockedObject = EasyMock.createMock(BindingResult.class); } public static BindingResultMockBuilder create() { return new BindingResultMockBuilder(); } public BindingResultMockBuilder withErrors(boolean hasErrors) { EasyMock.expect(mockedObject.hasErrors()).andReturn(hasErrors).anyTimes(); return this; } public BindingResultMockBuilder withAddAllErrors() { mockedObject.addAllErrors((Errors) EasyMock.anyObject()); EasyMock.expectLastCall().anyTimes(); return this; } public BindingResultMockBuilder withGetAllFieldErrors() { EasyMock.expect(mockedObject.getFieldErrors()).andReturn(new ArrayList<FieldError>()).anyTimes(); return this; } public BindingResultMockBuilder withRejectValue() { mockedObject.rejectValue((String) EasyMock.anyObject(), (String) EasyMock.anyObject()); EasyMock.expectLastCall().anyTimes(); return this; } }
Results
- The MockBuilder can be extended to create a new subclass for anything you need to mock.
- Large amounts of boilerplate code can be refactored into a simpler invocation of a Builder.
- Cleaner Unit Tests = Better Unit Tests.