Refactoring the tests that use too many mocks

In this recipe, we will take a look at a test that uses too many Mockito mocks. In this way, the test code becomes unreadable and unmaintainable. Since your test code is your living documentation, you should always remember to put a lot of effort into refactoring it until you can read it like a book.

Getting ready

For this recipe, we will again generate a new identity for a given person. Each person has an address, and that address has a street number. Since we are performing unit testing, we will check in isolation whether NewIdentityCreator properly executes its logic. It is responsible for creating a new name, new street number, and new siblings for the current person, as shown in the following code:

class NewIdentityCreator {

  public String createNewName(Person person) {
        return person.getName() + "_new";
    }

  public int createNewStreetNumber(Person person) {
        return person.getAddress().getStreetNumber() + 5;
    }

    public List<Person> createNewSiblings(Person person) {
        List<Person> newSiblings = new ArrayList<Person>();
        for(Person sibling : person.getSiblings()) {
          Person newPerson = new Person();
          person.setName(createNewName(sibling));
          person.setAddress(sibling.getAddress());
          person.setSiblings(sibling.getSiblings());
          newSiblings.add(newPerson);
          }      
        return newSiblings;
    }

}

Let's assume that we already have a test that verifies this functionality. We will not go through all of the test cases, but we will focus on the functionality of generating new siblings as follows:

public class OverMockingNewIdentityCreatorTest {
  
  NewIdentityCreator systemUnderTest = new NewIdentityCreator();

  
  @Test
  public void should_generate_new_siblings() {
    // given
    Person person = mock(Person.class);
    List<Person> oldSiblings = mock(List.class);  
    given(person.getSiblings()).willReturn(oldSiblings);
    Iterator<Person> personIterator = mock(Iterator.class);
    given(oldSiblings.iterator()).willReturn(personIterator);
    given(personIterator.hasNext()).willReturn(true, true, true, false);
    given(personIterator.next()).willReturn(createPersonWithName("Amy"),
                        createPersonWithName("John"),
                        createPersonWithName("Andrew"));

    // when
    List<Person> newSiblings = systemUnderTest.createNewSiblings(person);
    
    // then
    then(newSiblings).isNotSameAs(oldSiblings);
  }

  private Person createPersonWithName(String name) {
    Person person = new Person();
    person.setName(name);
    return person;
  }

}

The preceding test is badly written because it violates a few of the good practices related to Mockito and testing such as:

  • Don't mock a type you don't own: There are mocks of the Person and List classes created in the code that don't concern us. Imagine a case where the library that has either of those classes changes. Since we have its classes mocked, we will not see any difference and the tests will pass. Imagine what could happen on production once the real interactions take place instead of interactions between mocks—your application could crash. Another matter is that in order to use List by using mocks, you have to perform plenty of stubbing. Once you look at such a test, you don't actually know what's going on any longer.
  • Don't mock everything: The idea behind unit tests is to test in isolation. It does not mean that we have to mock everything out. The NewIdentityCreator class interacts with Person and performs iteration over its elements (via the iterator of the list). As you can see in the test, we've mocked all collaborating objects and stubbed all possible interactions. The question that arises now is: do we really test production code if there are no real interactions any longer? Change your viewpoint and try not to mock if possible.
  • Don't mock value objects: Of course, it all depends on the context, but in the vast majority of cases, you should not mock value objects. Being structures, they don't have any logic that could be stubbed, apart from getters and setters. Why would you want to stub them? You can use the builder or factory method pattern (check out Design Patterns: Elements of Reusable Object-Oriented Software, Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, available at http://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612), you can ask your IDE to help you, or you can use project Lombok (http://projectlombok.org/) to create value objects for you.

How to do it...

To properly rewrite a test that uses too many mocks, you have to perform the following steps:

  1. Identify all the types that were unnecessarily mocked (in our case, these are Person, List, and Iterator classes)
  2. Change mock creation to object creation where applicable (we will initialize the Person class and create an ordinary List)
  3. If your object creation seems complex or unreadable, it's worth creating a builder or a method that will build that object for you

Let's assume that we want to extract the Person object creation into a separate class to make the test code more readable (see the code repository on GitHub for exact implementation details). Take a look at the following code:

public class PersonBuilder {
  private String name;
  private Address address;
  private List<Person> siblings;

  public PersonBuilder name(String name) {
    this.name = name;
    return this;
  }

  public PersonBuilder address(Address address) {…}
  
  public PersonBuilder streetNumber(int streetNumber) {…}

  public PersonBuilder siblings(List<Person> siblings) {…  }

  public Person build() {
    Person person = new Person();
    person.setName(name);
    person.setAddress(address);
    person.setSiblings(siblings);
    return person;
  }
  
  public static PersonBuilder person() {
    return new PersonBuilder();
  } 
} 

How it works..

As you can see in the previous code, the PersonBuilder class has a static factory method to instantiate itself. It allows you to use less code and more ubiquitous language (check Domain-Driven Design: Tackling Complexity in the Heart of Software, Eric Evans, available at http://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215) in your tests. It has fields that are filled up with data during the building process. The Person class is created upon the build() method execution. For the sake of readability, we will not go into the details of AddressBuilder, but it follows the same pattern and sets a street address on the Address object.

Now that we have the builder ready, we can use it to rewrite the test as follows:

public class NewIdentityCreatorTest {

  NewIdentityCreator systemUnderTest = new NewIdentityCreator();

  @Test
  public void should_generate_new_siblings() {
    // given
    List<Person> oldSiblings = createSiblings();
    Person person = createPersonWithStreetNumberAndSiblingsAndName(oldSiblings);

    // when
    List<Person> siblings = systemUnderTest.createNewSiblings(person);

    // then
    then(siblings).doesNotContainAnyElementsOf(oldSiblings);
  }

  private Person createPersonWithStreetNumberAndSiblingsAndName(List<Person> siblings) {
    return person().streetNumber(10)
                   .siblings(siblings)
        .name("Robert")             
        .build();
  }

  private List<Person> createSiblings() {
    return asList(
        person().name("Amy").build(),
        person().name("John").build(),
        person().name("Andrew").build()
    );
  }
}

Now, the test looks much better and it can be used as a living documentation of your application.

There's more…

When working with legacy code or third-party software, you can come across very deeply nested structures that row in hundreds of lines of code. You don't own these value objects, so you can't change it in any way. Using these objects may be tedious, so it's important to have proper factory methods/builders to create them.

Of course, context is king and there might be cases in which a more pragmatic approach will be to not build the whole object using builders but to stub a very precisely defined chain of method execution. In Mockito, such stubbing is called deep stubbing, and the Answer implementation that allows you to set up such stubbing behavior is called Mockito.RETURNS_DEEP_STUBS. The chain violates the Law of Demeter (see Object-Oriented Programming: An Objective Sense of Style, K. Lieberherr, I. IIolland, A. Riel, available at http://www.ccs.neu.edu/research/demeter/papers/law-of-demeter/oopsla88-law-of-demeter.pdf.) since we're breaking the the friend of my friend is not my friend rule. Remember that you really need some legitimate reasons to use deep stubbing. In a well-designed codebase, you will not need to perform such actions. For educational purposes, let's take a look at the usage of deep stubs. We test the creation of a street number, where, in order to get it, we have to pass it through the Person.getAddress().getStreetNumber() chain of method execution, as shown in the following code:

public class DeepStubbingNewIdentityCreatorTest {
  NewIdentityCreator systemUnderTest = new NewIdentityCreator();  
  @Test
  public void should_generate_new_address_with_street_number() {
    // given
    Person person = mock(Person.class, RETURNS_DEEP_STUBS);
    given(person.getAddress().getStreetNumber()).willReturn(10);
    
    // when
    int newStreetNumber = systemUnderTest.createNewStreetNumber(person);
    
    // then
    then(newStreetNumber).isNotEqualTo(person.getAddress().getStreetNumber());
  }
}

When calling given(person.getAddress().getStreetNumber()).willReturn(10) under the hood, Mockito creates all the intermediary mocks. In this case, since Person is already a mock, Address is also created as a mock. In this way NullPointerException is not thrown when moving down the chain of method invocations. Remember that it is not a sign of good design when you need to use this feature of Mockito.

See also

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.39.60