Skip to content

Latest commit

 

History

History
702 lines (503 loc) · 29.3 KB

mocking.md

File metadata and controls

702 lines (503 loc) · 29.3 KB

Mock Objects

One of the main advantages of doing unit test is how much control we have over the class under test. However, some classes might depend on other classes. Or they might depend on external infrastructure, such as databases or webservices.

We might do our best to model our classes in such a way that infrastructure code is far away from our entities and business code. However, at some point, some of your classes will depend on on some other classes. For example, all SQL-related code might be encapsulated in a InvoiceDAO class. But parts of your system will depend on this InvoiceDAO class, which depends on a database. As a consequence, testing anything that depends (directly or indirectly) of an InvoiceDAO will require a database, even though our focus might not even be the database part of the system. As we saw before, this might incur in higher testing costs.

Generically speaking: we want to unit test a component A, that depends on another component B. The dependency on component B increases the costs of unit testing A.

This is where a mock object becomes handy. The idea is that we will create an object that will mimic the behaviour of component B ("it looks like B, but it is not B"). Given that we have full control over what this "fake B component" does, we can make it in such a way that unit testing A becomes less costly. In our example above, suppose that A is a plain-old Java class, B is a Data Access Object (and thus, it communicates with the database). If we simulate B by, say, returning a hard-coded list of two elements whenever the findAllInvoices() method is called, instead of going to the database, we remove the need for a database when testing A. If this seems all too abstract, the source code we will show soon will clarify everything.

The use of objects that simulate the behaviour of other objects has advantages:

  • The first one is that we have way more control. We can easily tell these objects what to do, without the need for complicated setups. If we want a method to throw an exception, we just tell the fake method to throw it; there is no need for complicated setups to "force" the dependency to throw the exception. If we want a method that returns a Calendar object dating of 2009, we just tell the fake method to do it; there is no need for complicated magic in your OS to make it go back in time.

(As an addition, note that how hard it usually is to "force" a class to throw an exception, or to "force" a class to return a fake date. This effort is close to zero when we simulate the dependencies).

  • Simulations are also faster. Suppose a dependency that communicates with a webservice or a database. A method in one of these classes might take a few seconds to process. On the other hand, if we simulate the dependency, it will not need to go to a database or to a webservice anymore; the simulation will simply return what it was configured to return, and it will cost nothing in terms of time.

Simulating dependencies is therefore a widely used technique in software testing, mainly to increase testability.

You might have noted that, although the title of this chapter is "mock objects", we have been using the word "simulation". As you will see in the remaining of this chapter, you may need different types of "simulation objects", according to your problem.

Meszaros, in his book, defines five different types: dummy objects, fake objects, stubs, spies, and mocks:

  • Dummy objects: Objects that are passed to the class under test, but they are never really used. This is a common case in business applications, where in many cases you need to fill a long list of parameters, but the test actually exercises just a few of them. Think of a unit test for a Customer class. Maybe this class depends on several other classes (Address, Last Order, etc). But a specific test case A wants to exercise the "last order business rule", and does not care much about which Address this Customer has. In this case, a tester would then set up a dummy Address object and pass it to the Customer class.

  • Fake objects: Fake objects have real working implementations of the class they simulate. However, they usually do the same task in a much simpler way. Imagine a fake database object that uses some sort of array list instead of a real database.

  • Stubs: Stubs provide hard-coded answers to the calls that are called during the test. In the examples above, where we used the word "simulation", we were actually talking about stub objects. Stubs do not know what to do if the test calls a method in which it was not programmed/setup for.

  • Spies: As the name suggests, spies "spy" a dependency. Imagine you need to know how many times a method X is called in a dependency; a spy thus observe all the interactions with the dependency, and records this information.

  • Mocks: Mock objects know what is expected of them, in terms of method calls they will (or will not) receive. For example, a mock object knows that method A should be called twice (and, say, for the first call it should return "1", and for the second call, it should throw an exception), or that method B should never be called.

In the rest of this chapter, we introduce the reader to Mockito, a popular mocking framework in Java. Although Mockito has "mocks" in the name, Mockito can be used for stubbing and spying as well. We also show, by means of a few examples, how simulations can help developers in writing unit tests more effectively.

{% hint style='tip'%} You can read more about the history of Mock Objects at http://www.mockobjects.com. {% endhint %}

Mockito

One of the most popular Java mocking/stubbing frameworks is Mockito (mockito.org). Mockito has a very simple API. Developers can setup stubs and/or define expectations in mock objects with just a few lines of code.

The most important methods one needs to know from Mockito are:

  • mock(<class>): creates a mock object/stub of a given class. The class can be retrieved from any class by <ClassName>.class.
  • when(<mock>.<method>).thenReturn(<value>): defines the behaviour when the given method is called on the mock. In this case <value> will be returned.
  • verify(<mock>).<method>: asserts that the mock object was exercised in the expected way.

Mockito is an extensive framework, and we point to reader to its documentation.

Stubbing

Let us learn how to use Mockito and setup stubs with a practical example.

Requirement: Low value invoices

The program must return all the issues invoices with value smaller than 100. The collection of invoices can be found in our database.

The following is a possible implementation of the requirement above. Note that the IssuedInvoices is a class responsible for retrieving all the invoices from the database.

import java.util.List;
import static java.util.stream.Collectors.toList;

public class InvoiceFilter {

  public List<Invoice> lowValueInvoices() {
    final var issuedInvoices = new IssuedInvoices();
    try {
      return issuedInvoices.all().stream()
              .filter(invoice -> invoice.value < 100)
              .collect(toList());
    } finally {
      issuedInvoices.close();
    }
  }

}

Without stubbing the IssueInvoices class, the InvoiceFilter test would need to also handle the database (making the unit testing more expensive). Note how the tests needs to open and close the connection (see the open() and closeDao() methods) and persist invoices in the database (see the many calls to invoices.save() in the filterInvoices test).

public class InvoiceFilterTest {
  private IssuedInvoices invoices;

  @BeforeEach public void open() {
    invoices = new IssuedInvoices();
  }

  @AfterEach public void closeDao() {
    if (invoices != null) invoices.close();
  }

  @Test
  void filterInvoices() {
    final var mauricio = new Invoice("Mauricio", 20);
    final var steve = new Invoice("Steve", 99);
    final var arie = new Invoice("Arie", 300);

    invoices.save(mauricio);
    invoices.save(steve);
    invoices.save(arie);

    final InvoiceFilter filter = new InvoiceFilter();

    assertThat(filter.lowValueInvoices()).containsExactlyInAnyOrder(mauricio, steve);
  }

}

This test is not even complete... We also need to reset the database after every test. Otherwise, the test will break in its second run, as there will be now four invoices with an amount smaller than 100 stored in the database! Remember that the database stores data permanently. So far, we never had to "clean our messes" in a test code, as all the objects we have created were always stored in-memory only.

{% hint style='tip'%} Did you notice the assertThat...containsExactlyInAnyOrder assertion we used? The assertion says it all: it makes sure that the list contains exactly the objects we pass, and in any order.

Such assertions do not come with JUnit 5. These assertions are part of the AssertJ project. AssertJ is a fluent assertions API for Java, giving us several interesting assertions that can come in handy, especially when dealing with lists or complex objects. We recommend the reader to get familiar with it! {% endhint %}

{% set video_id = "0WY7IWbANd8" %} {% include "/includes/youtube.md" %}

Let us now re-write the test. This time we will stub the IssuedInvoices class.

For that to happen, we first need to make sure the stub can be "injected" into the InvoiceFilter class. If you look at the previous implementation of the InvoiceFilter class, you will notice that the class instantiates the IssuedInvoices class on its own. If we are to use a stub, the class should allow the stub to be injected.

A simple refactor operator that receives the dependency via constructor, instead of instantiating it directly suffices:

import java.util.List;
import static java.util.stream.Collectors.toList;

public class InvoiceFilter {

  final IssuedInvoices issuedInvoices;

  public InvoiceFilter(IssuedInvoices issuedInvoices) {
    this.issuedInvoices = issuedInvoices;
  }
  public List<Invoice> lowValueInvoices() {
      return issuedInvoices.all().stream()
              .filter(invoice -> invoice.value < 100)
              .collect(toList());
  }

}

Let us now stub IssuedInvoices. Note that now our test does not need to do anything that is related to databases. The full control of the stub enables us to try different cases (even exceptional ones) very quickly:

import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;

public class InvoiceFilterTest {
    private final IssuedInvoices invoices = Mockito.mock(IssuedInvoices.class);
    private final InvoiceFilter filter = new InvoiceFilter(issuedInvoices);

    @Test
    void filterInvoices() {
      final var mauricio = new Invoice("Mauricio", 20);
      final var steve = new Invoice("Steve", 99);
      final var arie = new Invoice("Arie", 300);

      when(issuedInvoices.all()).thenReturn(asList(mauricio, arie, steve));

      assertThat(filter.lowValueInvoices()).containsExactlyInAnyOrder(mauricio, steve);
    }

}

Note how we setup the stub, using Mockito's when method. In this example, we tell the stub to return a list containing mauricio, arie, and steve (the three invoices we instantiate as part of the test case). The test then invokes the method under test, filter.lowValueInvoices(). As a consequence, the method under test invokes issuedInvoices.all(). However, at this point, issuedInvoices is actually a stub that solely returns the list with the three invoices. The method under test continues its execution, returns a new list with only the two invoices that are below 100, making the assertion to pass.

Note that, besides making the test easier to write, the use of stubs also made the test class more cohesive. The InvoiceFilterTest only tests the InvoiceFilter class. It does not test the u of the IssueInvoices class. Clearly, IssueInvoices deserves to be tested, but some place else, and by means of an integration test.

Note that a cohesive test has less chances of failing because of something else. In the old version, the filterInvoices test could fail because of a bug in the InvoiceFilter class or because of a bug in the IssuedInvoices class. The new tests can now only fail because of a bug in the InvoiceFilter class, and never because of IssuedInvoices. That is certainly handy, as a developer will spend less time debugging in case this test starts to fail.

Our new approach for testing InvoiceFilter is faster, easier to be written, and more cohesive.

{% set video_id = "kptTWbeLZ3E" %} {% include "/includes/youtube.md" %}

{% set video_id = "baunKy04deM" %} {% include "/includes/youtube.md" %}

From a developers perspective, the use of stubs enables them to develop their software, "without caring too much about external details". Imagine a developer working on this "Low value invoices" requirement. The developer knows that the invoices will come from the database. However, while developing the main logic of the requirement (i.e., the filtering logic), the developer "does not care about the database"; s/he only cares about the list of invoices that will come from it.

In other words, the developer only cares about the existence of a method that returns all the existing invoices. In object-oriented languages, that can be represented by means of an interface:

public interface IssuedInvoices {
  List<Invoice> all();
  void save(Invoice inv);
}

Having such an interface, the developer can then proceed to the InvoiceFilter and develop it completely. After all, its implementation never really depended on a database, but solely on the issued invoices. Look at it again:

public class InvoiceFilter {

  final IssuedInvoices issuedInvoices;

  public InvoiceFilter(IssuedInvoices issuedInvoices) {
    this.issuedInvoices = issuedInvoices;
  }
  public List<Invoice> lowValueInvoices() {
      return issuedInvoices.all().stream()
              .filter(invoice -> invoice.value < 100)
              .collect(toList());
  }
}

Once the InvoiceFilter and all its tests are done, the developer can then focus on finally implementing the IssuedInvoices class and its integration tests.

Once you get used to this way of developing, you will notice how your code will become easier to test. We will talk more about design for testability in future chapters.

Mocks and expectations

Suppose our system has a new requirement:

Requirement: Send low valued invoices to SAP

All low valued invoices should be sent to our SAP system. SAP offers a /sendInvoice webservice that receives invoices.

Let us follow the idea of using test doubles to facilitate the development of our production and test code. To that aim, let us create a SAP interface that represents the communication with SAP:

public interface SAP {
  void send(Invoice invoice);
}

We now need a class that will coordinate the process: retrieve the low valued invoices from the InvoiceFilter and pass them to the SAP service. Let us create a new SAPInvoiceSender class:

public class SAPInvoiceSender {

  private final InvoiceFilter filter;
  private final SAP sap;

  public SAPInvoiceSender(InvoiceFilter filter, SAP sap) {
    this.filter = filter;
    this.sap = sap;
  }

  public void sendLowValuedInvoices() {
    filter
      .lowValueInvoices()
      .forEach(invoice -> sap.send(invoice));
  }
}

Let us know test the SAPInvoiceSender class.

Note that, for this test, we now mock the InvoiceFilter class. After all, for the SAPInvoiceSender, InvoiceFilter class is "just a class that returns a list of invoices". As it is not the goal of the current test to test the filter itself, we should mock this class, as to facilitate the testing of the method under test

After the execution of the method under test (sendLowValuedInvoices()), we should expect that the sap mock received mauricio's, steve's, and aries invoices. For that, we use Mockito's verify() method:

public class SAPInvoiceSenderTest {

  private static final InvoiceFilter filter = mock(InvoiceFilter.class);
  private static final SAP sap = mock(SAP.class);
  private static final SAPInvoiceSender sender = new SAPInvoiceSender(filter, sap);

  @Test
  void sendToSap() {
    final var mauricio = new Invoice("Mauricio", 20);
    final var steve = new Invoice("Steve", 99);
    final var arie = new Invoice("Arie", 300);

    when(filter.lowValueInvoices()).thenReturn(asList(mauricio, steve, arie));

    sender.sendLowValuedInvoices();

    verify(sap).send(mauricio);
    verify(sap).send(steve);
    verify(sap).send(arie);
  }
}

Note how we defined the expectations of the mock object. We "knew" exactly how the InvoiceFilter class had to interact with the mock. When the test is executed, Mockito will check whether these expectations were met, and fail the test if they were not. (If you want to test it, simply comment out the forEach... line in the sendLowValuedInvoices and see the test failing).

This example illustrates the main difference between simply stubbing and mocking. Stubbing means simply returning hard-coded values for a given method call; mocking means not only defining what methods do, but also explicitly define how the interactions with the mock should be.

Mockito actually enables us to define even more specific expectations. For example, see the expectations below:

verify(sap, times(3)).send(any(Invoice.class));
verify(sap, times(1)).send(mauricio);
verify(sap, times(1)).send(steve);
verify(sap, times(1)).send(arie);

These ones are more restrictive than the ones we had before. We now expect the SAP mock to have its send method invoked precisely three times (for any given Invoice). We then expect the send method to called once for the mauricio invoice, once for the steve invoice, and once for the arie invoice. We point the reader to Mockito's manual for more details on how to configure expectations.

You might be asking yourself now: Why did you not put this new SAP u inside of the existing InvoiceFilter class?

If we were do it, the lowValueInvoices method would then be both a "command" and a "query". By "query", we mean that the method returns data to the caller; but "command", we mean that the method also perform an action in the system. Mixing both concepts in a single method is not a good idea, as it may confuse developers who will eventually call this method. How would they know this method had some extra side-effect, besides just returning the list of invoices?

If you want to read more about it, search for Command-Query Separation, or CQS, a concept devised by Bertrand Meyer.

How to stub static methods (or with APIs you do not control)

Imagine the following requirement:

Requirement: Christmas Discount

The program must give a 15% discount to the raw amount of an order if current date is Christmas day. No discounts otherwise.

A possible implementation for this requirement is as follows:

public class ChristmasDiscount {

public double applyDiscount(double rawAmount) {
    Calendar today = Calendar.getInstance();

    double discountPercentage = 0;
    boolean isChristmas = today.get(Calendar.DAY_OF_MONTH) == 25 &&
          today.get(Calendar.MONTH) == Calendar.DECEMBER;

    if(isChristmas)
      discountPercentage = 0.15;

    return rawAmount - (rawAmount * discountPercentage);
  }

}

The implementation is quite straightforward. And given the characteristics of the class, unit testing seems to be a perfect fit for testing it. The question is: how can we write unit tests for it? To test both cases (i.e., is Christmas, is not Christmas), we need to be able to control/stub the Calendar class, so that it returns the dates we want.

We can then ask a more specific question: how can we stub the Calendar API? You might have noted that the call to Calendar.getInstance() is a static call. Mockito does not allow us to stub static methods (although some other more magical mock frameworks do). Static calls are indeed enemies of testability, as they do not allow for easy stubbing.

In such cases, a pragmatic solution is to create an abstraction on top of the static call. The abstraction encapsulates the "not-so-easy-to-be-stubbed" method, and offers an "easy-to-be-stubbed" method to the rest of the program. For this particular case, a Clock abstraction can do the job:

import java.util.Calendar;

public interface Clock {
    Calendar now();
}

With this interface in hands, we can follow the same approach as before. Let us inject Clock to the ChristmasDiscount class:

import java.util.Calendar;

public class ChristmasDiscount {

  private final Clock clock;

  public ChristmasDiscount(Clock clock) {
    this.clock = clock;
  }

  public double applyDiscount(double rawAmount) {
    Calendar today = clock.now();

    double discountPercentage = 0;
    boolean isChristmas = today.get(Calendar.DAY_OF_MONTH) == 25 &&
          today.get(Calendar.MONTH) == Calendar.DECEMBER;

    if(isChristmas)
      discountPercentage = 0.15;

    return rawAmount - (rawAmount * discountPercentage);
  }
}

With Clock being an interface, we can stub it the way we want and/or need:

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.Calendar;
import java.util.GregorianCalendar;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

public class ChristmasDiscountTest {

    private final Clock clock = Mockito.mock(Clock.class);
    private final ChristmasDiscount cd = new ChristmasDiscount(clock);

    @Test
    public void christmas() {
      Calendar christmas = new GregorianCalendar(2015, Calendar.DECEMBER, 25);
      when(clock.now()).thenReturn(christmas);

      double finalValue = cd.applyDiscount(100.0);
      assertEquals(85.0, finalValue, 0.0001);
    }

    @Test
    public void notChristmas() {
      Calendar christmas = new GregorianCalendar(2015, Calendar.JANUARY, 25);
      when(clock.now()).thenReturn(christmas);

      double finalValue = cd.applyDiscount(100.0);
      assertEquals(100.0, finalValue, 0.0001);
    }
}

Note again that we were able to develop the main logic of the requirement without depending on the concrete implementation of Clock. For completeness, let us implement Clock:

public class DefaultClock implements Clock {
    @Override
    public Calendar now() {
      return Calendar.getInstance();
    }
}

Creating abstractions on top of dependencies that you do not own, as a way to gain more control, is a common technique among developers.

One might argue: But won't that increase the overall complexity of my system? After all, it is one more abstraction to be maintained? Yes. Indeed, complexity grows up whenever we add new abstractions in our software. And that is what we are doing here. However, the point is: does the ease in testing the system that we get from adding this abstraction pay off the cost of the increased complexity? Often, the answer is yes, it does pay off.

When to mock/stub?

Mocks and stubs are a useful tool when it comes to ease the process of writing unit tests. However, as expected mocking too much might also be problem. We do not want to mock a dependency that should not be mocked. Suppose you are testing class A, which depends on a class B. Should we mock/stub B?

Pragmatically, developers often mock/stub the following types of dependencies:

  • Dependencies that are too slow: If the dependency is too slow, for any reason, it might be a good idea to simulate that dependency.

  • Dependencies that communicate with external infrastructure: If the dependency talks to (external) infrastructure, it might be too complex to be set up. Consider stubbing it.

  • Hard to simulate cases: If we want to force the dependency to behave in a hard-to-simulate way, mocks/stubs can help. A common example is when we would like the dependency to throw an exception. Forcing an exception in the real dependency might be tricky, but easy to do in a stub/mock.

On the other hand, developers tend not to mock/stub:

  • Entities. In business systems, it is quite common that entities depend on other entities, e.g., a Order depends on OrderItem. When testing Order, developers tend not no stub OrderItem. The reason is that OrderItem is also probably a well-contained class that is easy to be set up. Mocking it would take more time than actually using the real implementation. Exceptions can be made for heavy entities.

  • Native libraries and utility methods. It is not common to mock/stub libraries that come with our programming language and utilities methods. For example, why would one mock ArrayList or a call to String.format? As exemplified with the Calendar example above, any library or utility methods that harm testability can be abstracted way.

Ultimately, remember that whenever you mock, you reduce the reality of the test. It is up to you to understand this trade-off.

Exercises

The code implemented in this chapter can be found at the invoice, invoicestubbed, and invoicesap packages in the code examples repository.

Exercise 1. See the following class:

public class OrderDeliveryBatch {

  public void runBatch() {

    OrderDao dao = new OrderDao();
    DeliveryStartProcess delivery = new DeliveryStartProcess();

    List<Order> orders = dao.paidButNotDelivered();

    for (Order order : orders) {
      delivery.start(order);

      if (order.isInternational()) {
        order.setDeliveryDate("5 days from now");
      } else {
        order.setDeliveryDate("2 days from now");
      }
    }
  }
}

class OrderDao {
  // accesses a database
}

class DeliveryStartProcess {
  // communicates with a third-party webservice
}

Which of the following Mockito lines would never appear in a test for the OrderDeliveryBatch class?

  1. OrderDao dao = Mockito.mock(OrderDao.class);
  2. Mockito.verify(delivery).start(order); (assume order is an instance of Order)
  3. Mockito.when(dao.paidButNotDelivered()).thenReturn(list); (assume dao is an instance of OrderDao and list is an instance of List<Order>)
  4. OrderDeliveryBatch batch = Mockito.mock(OrderDeliveryBatch.class);

Exercise 2. You are testing a system that triggers advanced events based on complex combinations of Boolean external conditions relating to the weather (outside temperature, amount of rain, wind, ...). The system has been cleanly designed and consists of a set of cooperating classes that each have a single responsibility. You create a decision table for this logic, and decide to test it using mocks. Which is a valid test strategy?

  1. You use mocks to support observing the external conditions.
  2. You create mock objects to represent each variant you need to test.
  3. You use mocks to control the external conditions and to observe the event being triggered.
  4. You use mocks to control the triggered events.

Exercise 3. Below, we show the InvoiceFilter class. This class is responsible for returning all the invoices that have an amount smaller than 100.0. It makes use of the InvoiceDAO class, which is responsible for the communication with the database.

public class InvoiceFilter {

    private InvoiceDao invoiceDao;

    public InvoiceFilter(InvoiceDao invoiceDao) {
        this.invoiceDao = invoiceDao;
    }

    public List<Invoice> filter() {
        List<Invoice> filtered = new ArrayList<>();
        List<Invoice> allInvoices = invoiceDao.all();

        for(Invoice inv : allInvoices) {
            if(inv.getValue() < 100.0)
                filtered.add(inv);
        }

        return filtered;
    }
}

Which of the following statements is false about this class?

  1. Integration tests would help us achieve a 100% branch coverage, which is not possible solely via unit tests.
  2. Its implementation allows for dependency injection, which enables mocking.
  3. It is possible to write completely isolated unit tests for it by, e.g., using mocks.
  4. The InvoiceDao class (a direct dependency of the InvoiceFilter) itself should be tested by means of integration tests.

Exercise 4. Class A depends on a static method in another class B. Suppose you want to test class A, which approach(es) can you take to be able to test properly?

  1. Mock class B to control the u of the methods in class B.

  2. Refactor class A, so the outcome of the method of class B is now used as an parameter.

  3. Only approach 1.

  4. Neither.

  5. Only approach 2.

  6. Both.

Exercise 5. We should not forget what we saw in the previous chapter. Apply boundary testing techniques for the InvoiceFilter example discussed in this chapter.

References