1 Extreme Programming Extremism
Dark chocolate is good for you. It’s good for your heart and may prevent cancer. But no one should take consumption of dark chocolate to the extreme and eat it for breakfast, lunch, and dinner. That wouldn’t just be silly, it would be decidedly unhealthy. Yet a current fad in software development goes under the moniker of “extreme programming” where otherwise good ideas are taken to extremes. Code reviews are good, so have someone sitting next you continuously code reviewing every keystroke you type. Or, the subject of this essay, testing code in isolation is good and using mocks is useful, so obsessively test every class in isolation while mocking every other class it depends on. It is the goal of this essay to argue for a more pragmatic, moderate approach to unit testing.
1.1 Drawbacks to Excessive Mocking
Mocking tends to expose internal implementation details of the class under test. The test then isn’t just testing that the class does what it’s supposed to do, but that it accomplishes its functionality in a particular way.
In compiler development, we lived and died by the “As-If” rule. It didn’t matter what transformation the optimizer did to a program if the user couldn’t detect the transformation (except that his program ran faster). When mocking exposes implementation details, then making changes to those details, even if they otherwise maintained the “As-If” rule, becomes burdensome because now you have to fix tests even though the functionality hasn’t changed.
Worse, every time you must modify a test to accommodate a change, you run the risk of breaking the test so that it no longer actually tests the functionality. I can’t begin to count the number of times I’ve found tests that didn’t actually test anything. There are no tests to verify the tests are working (except for bugs showing up in live code)!
1.2 Unit vs Integration Testing
Strict unit testing does not test whether components actually work together. By only doing integration testing in functional tests that require working staging environments, productivity is impacted because such tests are inconvenient and slow to run, requiring a properly configured stage, a working network connection, VPN, and other potential points of failure that have nothing to do with the code actually under test.
2 Pragmatic Testing
I prefer a more pragmatic approach to unit testing that blurs the line between unit and integration testing, but in a disciplined manner. I prefer to use a full set of tools to achieve my testing goals, whether they be mocks, spies, simulators, or what have you, rather than restrict myself to some ideological extreme.
2.1 Talking to the Outside World
The absolute boundary for testing code in isolation is downstream services, databases, and upstream infrastructure. These must be mocked one way or another to avoid having to configure a staging environment for unit tests to work or initializing all of Spring Boot and any other infrastructure in use. How specifically this can be accomplished will be discussed below in the discussion of using the SpringJunit4ClassRunner.
2.2 Green Path Testing
In the extreme unit testing, any object the test subject depends on gets mocked. Hopefully, those mocks will mock what the dependent object would have done or returned had it been called. All too often, I’ve seen mocks that only partially mock the behavior of the dependent objects, or worse, just plain do the wrong thing (probably because the actual behavior of the dependent object has changed, but the mock was not changed).
If the dependent object’s class has itself been thoroughly tested, and it isn’t something like a client of a downstream service or a database repository, it would actually be more reliable to let the test subject call the actual dependent object’s methods. Now you know that the test object is getting the result that the real dependent object would return, because you’re calling the real dependent object’s methods.
This is not without exceptions, and the exceptions may at times be a judgment call. If the dependent object has non-deterministic behavior, such as returning time of day, or a random number, then mocks are a valuable tool to test the test object getting particular values that aren’t accessible deterministically by calling the real method.
2.3 Exception Testing
Testing for responses to exceptions almost always require mocking or spying because the test subject is being tested for its response to something going wrong, usually unpredictably, with or in the dependent objects.
3 SpringJunit4ClassRunner.class
This section will discuss how to use the SpringJunit4ClassRunner.class to implement my vision of pragmatic testing outlined above for a REST service.
3.1 Annotating the Test Class
Here is how I annotate a test for a Spring component (AKA bean) that is created in a “request” scope:
@RunWith(SpringJunit4ClassRunner.class) @ContextConfiguration(classes={TestConfiguration.class}) @WebAppConfiguration class SubjectTest { @Inject Subject subject; @Test Public void testMethod { … subject.method() … } }
3.1.1 @RunWith(SpringJunit4ClassRunner.class)
This class runner let’s Spring perform injection (see @ContextConfiguration below) as well as letting the test use Mockito.
3.1.2 @ContextConfiguration
This annotation points to a Spring configuration class, discussed below, to configure what beans are configured for injection.
3.1.3 @WebAppConfiguration
This annotation effectively makes the test run in a “request” scope.
3.1.4 @Inject
The component under test is injected into the test. Other components may also be injected into the test as needed (should mocking or spying be needed – this will be discussed below). If the injected components have any injected components, they will be created and injected into them, and so on recursively, as is normal in a Spring application.
3.2 TestConfiguration Annotations
The TestConfiguration class is annotated thusly:
@Configuration @ComponentScan( basePackages="com.company.service", excludeFilters={ @ComponentScan.Filter(type=FilterType.REGEX, pattern="com.company.service.serv.*"), @ComponentScan.Filter(type=FilterType.REGEX, pattern="com.company.service.init.*")}) class TestConfiguration { ……… }
3.2.1 @Configuration
It’s a Spring configuration class.
3.2.2 @ComponentScan
Scan packages for the components that may be injected by Spring. I point basePackages to the root package, then exclude those I don’t want to actually be scanned. The ones I don’t want scanned are those that invoke downstream services and those that configure the application as a Spring Boot application as well as any other infra structure packages. I’m isolating my service code from this downstream and upstream stuff.
3.3 TestConfiguration Beans
Beans need to be configured here for any of the downstream services or databases, or that are provided by infrastructure. A number of approaches can be used here.
3.3.1 Actual Implementations
When it’s an object that would be provided by the infrastructure, just allocate an actual object:
@Bean InfraType infraType() { InfraType infraType = new InfraTypeImpl(); infraType.setProperty(1234); return infraType(); }
3.3.2 A Mock Object
A mock object can be returned. Then the individual tests must configure the mock as they like.
@Bean DownstreamService downstreamService() { return Mockito.mock(DownstreamService.class); }
3.3.3 A Simulated Object
This is the most controversial approach and can be abused. But some simulating of downstream services is very useful, especially for green path tests. When I do this, I prefer to spy on the simulated object so that whenever the default behavior of the simulation is insufficient (or a failure mode needs to be tested), the usual Mockito methods can be used to override the simulated behavior. This avoids the problem with simulation being inflexible or not covering all situations.
@Bean ResourceRepository resourceRepository() { Return Mockito.spy(new ResourceRepositorySimulator()); }
3.3.4 ResourceRepository Simulator Example
A good example of this technique is a database repository with a JpaRepository-like interface which is particularly easy to simulate. This makes integration testing a persistence service particularly easy. In the following sample code, ResourceRepository is an extension of the JpaRepository interface.
public class ResourceRepositorySimulator implements ResourceRepostory { // these Maps hold my simulated in-memory repository Map<String, ResourceEntity> idRepository; MultivaluedMap<String, ResourceEntity> secondaryKeyRepository; @Override public <S extends ResourceEntity> S save(S arg0) { String id = arg0.getId(); if (idRepository.containsKey(id)) { // get the previous persisted entity and remove it from // all the repository data structures ResourceEntity previous = idRepository.get(id); idRepository.remove(id); secondaryKeyRespository.remove(previous.getSecondaryKey(), previous); } // add the new entity and persist it in all the repository // data structures ResourceEntity clone = (ResourceEntity)arg0.clone() idRepository.put(id, clone); secondaryKeyRepository.add(clone.getSecondaryKey(), clone); return (S) arg0.clone(); } @Override public ResourceEntity findResourceById(String id) { return idRepository.get(id).clone(); } @Override public Page<ResourceEntity> findResourcesBySecondaryKey( String secondaryKey, String tertiaryKey, Pageable pageable) { if (ownerIdRepository.containsKey(secondaryKey)) { List<ResourceEntity> list = secondaryKeyRepository.get(secondaryKey); PageImpl<ResourceEntity> page = filterAndPageResult(list, tertiaryKey, pageable); return page; } else { return new PageImpl<ResourceEntity>( Collections.emptyList()); } } ……… }
Because I use Mockito.spy() on the simulator’s bean, any test that needs to override the default behavior of the simulator can use Mockito to do so. For example, to simulate getting a DataAccessException from the repository:
@Inject private ResourceRepository resourceRepository; @Inject Private ServiceImpl service; @Test public void testFindById_DataAccessException() { Mockito.doThrow(new DataAccessException()).when(resourceRepository) .findResourceById(Mockito.anyString()); try { // test what happens if the service implemention gets an // exception from ResourceRepository service.findById("abc"); Assert.fail("Expected exception not thrown") } catch (DataAccessException e) { // expected exception return; } }
3.3.5 Cryptography Simulator Example
For something like a cryptography server used to encrypt and decrypt social security numbers or the like for storage in a database, it is not necessary for the simulator to implement the same encryption algorithm as the actual server (it likely isn’t even practical). The simulation can use something simple like Base64 (or rot13 for that matter). All that’s important is that decrypt(encrypt(“string”)) produce the value “string” again.
However, for a test that needs to start with an encrypted value and produce an unencrypted value, it’s undesirable for that test to embed in it knowledge of the particular encryption algorithm by embedding an encrypted value in it. Instead, mocking can be used.
In TestConfiguration, the cryptography bean is configured like this:
@Bean Cryptographer cryptographer() { return Mockito.spy(new CryptographerSimulator()); }
Then in the test class:
@Inject private Cryptographer cryptographer; @Test Public void test() { Mockito.doReturn("123-45-6789") .when(cryptographer) .decrypt(Mockito.eq(“encrypted_value”)); ……… }
Now the test can have the test value “encrypted_value” in it and have it get decrypted to a particular social security number without having knowledge of the simulator’s encryption algorithm, while tests which start with a plain text value and then encrypt that can just rely on the simulator.
4 Integration Testing REST Services
Part 2 will specifically look at integration testing REST service calls in Junit.