Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Use Mock Objects Judiciously

DZone's Guide to

Use Mock Objects Judiciously

Before using mock frameworks for testing, it's important to know about the issues they can create in dynamic projects with shifting requirements.

· DevOps Zone ·
Free Resource

Easily enforce open source policies in real time and reduce MTTRs from six weeks to six seconds with the Sonatype Nexus Platform. See for yourself - Free Vulnerability Scanner. 

Using mocks and mock frameworks is a common technique for writing tests, but people starting to use it very rarely consider the difficulties they are going to face in a dynamic project where requirements change very often, and when the tests have to be maintained over time by other people in the team. This article aims to give some insights on good and bad practices using mocks with the help of Java examples.

Mock objects are a special type of objects used to replace real objects for testing. They belong to the well-known testing objects called "test doubles," as described by Gerard Meszaros at xunitpatterns.com. The same terminology has been referenced by Martin Fowler in an article called Mocks Aren't Stubs: "Mocks are what we are talking about here: objects pre-programmed with expectations which form a specification of the calls they are expected to receive."

The mock satisfies the interface of the secondary object providing the ability to define expectations, specifying the way the mocked methods are called in a particular test scenario. Expectations may include verification of input parameters and output result, but also if a method was called once or many times. In general, mocks help in isolating the dependencies of an object under test from other classes. What makes them different from the classical state verification approach is that they do not, or at least not only, test whether the method under test worked correctly according to its contract definition, but rather verify the expectations defined for the mocks used by the particular test use case. In other words, mock objects enable a different approach to verification called behavior verification, as opposed to state verification.

There are many Java frameworks that provide ready-to-use constructs for mocking. Some of the most popular are:

The examples in this article are based on Mockito, but it can be any other mocking framework, or even a company's internal custom solution. The application used for this demonstration represents a standard use case where an internal service loads data from an external one and uses it for its own purposes. The external service is hidden behind an API containing a single interface:

/**
 * Loads data from a remote data repository.
 */
public interface ExternalDataService {

  /**
   * Loads data from a remote repository based on a list of identifiers and
   * returns the data serialized into text values.
   * 
   * @param ids
   *            A list of data identifiers
   * 
   * @return List of objects represented as textual values.
   */
  List<String> loadData(Collection<String> ids);

}

The application is comprised of an internal service responsible for loading the data and translating it into domain objects. The internal service is represented by an interface and one simplistic implementation for demonstration purposes:

/**
 * Loads data stored into application's domain objects.
 */
public interface InternalDataService {

  /**
   * Loads a list of domain objects based an a list of identifiers passed.
   * 
   * @param ids
   *            List of object identifiers to be serached for
   * @return List of domain objects corresponding to the identifiers passed for
   *         search
   */
  List<DomainObject> loadDomainObjects(List<String> ids);

}

The service implementation sends a request to a remote data storage, loads the data objects in a textual format, and parses and converts them into internal domain objects.

/**
 * Internal data service implementation. Loads data from an external repository
 * using its interface,
 */
public class InternalDataServiceImpl implements InternalDataService {

  /**
   * External data service
   */
  private ExternalDataService dataService;

  /**
   * Builder used for domain objects construction
   */
  private DomainObjectBuilder domainObjectBuilder;


  public InternalDataServiceImpl(ExternalDataService dataService, DomainObjectBuilder domainObjectBuilder) {
    this.dataService = dataService;
    this.domainObjectBuilder = domainObjectBuilder;
  }

  public List<DomainObject> loadDomainObjects(List<String> ids) {
    List<String> objectsAsString = dataService.loadData(ids);
    List<DomainObject> domainObjects = objectsAsString.stream().map(domainObjectBuilder::build)
    .collect(Collectors.toList());

    return domainObjects;
  }

}

The service class depends on two additional classes that can be found in my GitHub repository. Now, let's write a JUnit Mockito test to check the correctness of the internal service:

/**
 * InternalDataService Mock based test
 */
public class InternalDataServiceMockTest {

  private InternalDataService internalDataService;

  @Mock
  private ExternalDataService mockDataService;

  private DomainObjectBuilder domainObjectBuilder = new DomainObjectBuilderImpl();

  @Before
  public void setUp() {
    MockitoAnnotations.initMocks(this);
    internalDataService = new InternalDataServiceImpl(mockDataService, domainObjectBuilder);
  }

  @Test
  public void testLoadData() {
    List<String> ids = Arrays.asList("42", "1984");

    // Expectations
    when(mockDataService.loadData(ids)).thenReturn(Arrays.asList("42:Douglas Adams", "1984:George Orwell"));

    // Exercise
    List<DomainObject> loadedObjects = internalDataService.loadDomainObjects(ids);

    // Verify expectations
    verify(mockDataService, times(1)).loadData(ids);

    // Verify state
    List<DomainObject> expectedResult = Arrays.asList(new DomainObjectImpl("42", "Douglas Adams"),
    new DomainObjectImpl("1984", "George Orwell"));
    Assert.assertEquals(expectedResult, loadedObjects);
  }
}

The external service is mocked in the example using an annotation, and there are a few reasons to use a Mock object. It is part of an external API and abstracts the connection to an external service. This means that by using a mocked object instead of the real one, I have removed the dependency to something external to my test, which allows me to implement an isolated test for the internal service's logic. You have probably noticed that I have not provided a mock for the second dependency of the class - DomainObjectBuilder. The reason for that is I do not want to mock everything; my preference is to keep the test simple and to mock only dependencies that make my testing easier in terms of external dependencies. The builder class is part of my application. Normally, I would also create a JUnit test for it, which will help me to avoid noise in the service being tested in terms of bugs coming from dependency classes. Notice the steps comprising a particular test method:

  • Setup - initialize the object under test;

  • Setup expectations - define expectations for the mocked object;

  • Exercise - test the object, call the method under test;

  • Verify expectations - check if the mocked methods were invoked as expected;

  • Verify state - check the if the state of the object under state is the expected one;

  • Teardown - clean up resources.

Now imagine a real situation where we realize that we are not calling the external service the right way. There is an additional requirement to call the external service with a list of unique ids and not to expect from the external service to remove duplicate ids. The implementation of the service under test changes but the interface remains the same:

public List<DomainObject> loadDomainObjects(List<String> ids) {
  Set<String> uniqueIds = new HashSet<>(ids);
  List<String> objectsAsString = dataService.loadData(uniqueIds);
  List<DomainObject> domainObjects = objectsAsString.stream().map(domainObjectBuilder::build)
  .collect(Collectors.toList());

  return domainObjects;
}

I am running the test again, and it fails although I haven't changed the interface at all. It seems that I have tested the implementation during the Verify Expectations phase:

    // Verify expectations
    verify(mockDataService, times(1)).loadData(ids);

The problem in my test is that the expectation defined is too restrictive with respect to input parameters. In order to fix this, I can change the expectation as follows:

  //Verify expectations
  verify(mockDataService, times(1)).loadData(any());

One of the mistakes developers tend to make using mocks is fixed: Tight coupling of unit tests to the implementation of the code being tested should be avoided by testing the method's contract, not its implementation.

But we are not done with this. In the test, we have two verification steps:

   // Verify expectations
    verify(mockDataService, times(1)).loadData(ids);

    // Verify state
    List<DomainObject> expectedResult = Arrays.asList(new DomainObjectImpl("42", "Douglas Adams"),
    new DomainObjectImpl("1984", "George Orwell"));
    Assert.assertEquals(expectedResult, loadedObjects);

First, the behavior of the object under test is checked, and afterwards, its state. But this seems to be a bit too much. If the final state is correct, do I really need to know how many times an internal service was called? I could rely only on the state verification in this test and end up with a simpler test which will be easier to read and maintain. One more commonly disregarded mistake using Mocks was fixed: prefer state verification when possible, do not complicate tests with both state and behavior verification if not needed! This might lead to increased maintenance time and complexity.

In the example above, the mocked object was practically turned into a Stub by providing hardcoded return data, but omitting the behavior verification. This is another disregarded fact by developers: mocking frameworks can be used to create various testing objects; "test doubles," not only mocks.

One question that naturally arises here is: do we need mocks in general, or we can validate the correctness of our tests using only Stubs and state verification? To answer this question, let's extend our example with an additional requirement. The external service has a requests limit and we are not allowed to hit it very often. That's why we decide to use a local cache for already loaded objects:

/** 
 * Cache interface
 *
 * @param <K>
 *            Cache key type
 * @param <V>
 *            Cache value type
 */
public interface Cache<K, V> {

    /**
     * Store the value into the cache.
     * 
     * @param chacheKey
     *            Cache key
     * @param value
     *            Cache value
     */
    void addObject(K chacheKey, V value);

    /**
     * Find and return an object from the cache
     * 
     * @param chacheKey
     *            Cache key
     * @return Cache value found or null
     */
    V getObject(K chacheKey);

    /**
     * Clear the cache, remove all stored values
     */
    void clear();
}

...

public class LocalCache implements Cache<String, DomainObject> {

    private Map<String, DomainObject> chachedValues = new HashMap<String, DomainObject>();

    @Override
    public void addObject(String cacheKey, DomainObject value) {
      chachedValues.put(cacheKey, value);
    }

    @Override
    public DomainObject getObject(String chacheKey) {
      return chachedValues.get(chacheKey);
    }

    @Override
    public void clear() {
      chachedValues.clear();
    }
}  

The local cache is integrated with the internal service using the Decorator pattern. The decorator implementation can be seen in my GitHub repository. We are interested in the JUnit test now, and two test use cases:

  • Initial state - the cache is empty; data is loaded from the external service and stored into the cache after loaded. The test is based only on state verification, as for this particular use case, we can easily check if the empty cache was filled initially with the data after calling the method under test:

public class InternalDataServiceCacheDecoratorTest {

  private InternalDataServiceCacheDecorator internalDataServiceCacheDecorator;

  @Mock
  private InternalDataService mockDataService;

  private Cache<String, DomainObject> localCache = new LocalCache();

  @Before
  public void setUp() {
    MockitoAnnotations.initMocks(this);
    internalDataServiceCacheDecorator = new InternalDataServiceCacheDecorator(mockDataService, localCache);
  }

  @Test
  public void testLoadData() {
    List<String> ids = Arrays.asList("42", "1984");

    // Expectations
    when(mockDataService.loadDomainObjects(any())).thenReturn(Arrays
    .asList(new DomainObjectImpl("42", "Douglas Adams"), new DomainObjectImpl("1984", "George Orwell")));

    // Exercise
    List<DomainObject> loadedObjects = internalDataServiceCacheDecorator.loadDomainObjects(ids);

    // Verify state
    List<DomainObject> expectedResult = Arrays.asList(new DomainObjectImpl("42", "Douglas Adams"),
    new DomainObjectImpl("1984", "George Orwell"));
    Assert.assertEquals(expectedResult, loadedObjects);
    Assert.assertNotNull(localCache.getObject("42"));
    Assert.assertNotNull(localCache.getObject("1984"));
  }
}
  • Partial storage - part of the data is available in the cache while the other part should be loaded from the external service. Even if we check if the objects are available in the cache after the method execution, we can't be sure if the data was partly loaded from the cache or entirely loaded from the external service. This use case is an example where additional behavioral verification is needed. The behavior verification step is added in order to test that the request sent to the external service contains only the ids not found in the cache:

@Test
public void testLoadDataWithOneElementCached() {
  List<String> ids = Arrays.asList("42", "1984");

  //Data setup
  localCache.addObject("42", new DomainObjectImpl("42", "Douglas Adams"));

  // Expectations
  when(mockDataService.loadDomainObjects(Arrays.asList("1984")))
  .thenReturn(Arrays.asList(new DomainObjectImpl("1984", "George Orwell")));

  // Exercise
  List<DomainObject> loadedObjects = internalDataServiceCacheDecorator.loadDomainObjects(ids);

  // Verify expectations
  verify(mockDataService, times(1)).loadDomainObjects(Arrays.asList("1984"));

  // Verify state
  List<DomainObject> expectedResult = Arrays.asList(new DomainObjectImpl("42", "Douglas Adams"),
  new DomainObjectImpl("1984", "George Orwell"));
  Assert.assertEquals(expectedResult, loadedObjects);
  Assert.assertNotNull(localCache.getObject("42"));
  Assert.assertNotNull(localCache.getObject("1984"));
}

The purpose of this example was to demonstrate a test use case that can't be implemented without behavioral verification. What we've learned is: use behavioral verification in situations where state verification is not enough to validate the correctness of the object under test.

The ideas and practices defined in this post are based on the concept of Test Driven Development. There is an alternative approach which is based entirely on the usage of mock objects, but it is out of the scope of this post. Development teams starting a new project have to first choose the implementation and testing approach, and once done, negotiate some good practices, which this article demonstrates in order to keep testing efficient and maintainable. In general, what the article says is:

  • Test interfaces and method contracts, not implementations;
  • Use only state verification when behavior verification is not needed;

  • Apply behavioral verification in situations when the state verification is not enough.

Automate open source governance at scale across the entire software supply chain with the Nexus Platform. Learn more.

Topics:
mock ,java ,junit ,testing ,devops

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}