When to use mocks



After two article series where I preached against the use of mocks in tests (first one, second one), I thought I would do a post which outlines situations where they are justified. This article is based on my recent Pluralsight course about Pragmatic Unit Testing.

Inner-system vs inter-system communications

The main issue with the use of mocks is that they encourage focusing on collaborations. That, in turn, often leads to coupling your tests to implementation details as those collaborations are usually not part of the SUT’s public API.

But that is not always the case. Sometimes, the way a system communicates with other systems does constitute its API. Let’s see how it is so.

There are two different types of communication that usually happen in enterprise applications. The first one is communication inside the application and the second one is when your application talks to other applications. We can call them inner-system and inter-system communications respectively. The boundary of a single application is usually determined by the boundary of the process hosting it.

When to use mocks: Inner-system communications

Inner-system communications

Inner-system communications are implementation details. The collaborations your domain classes go through in order to achieve some goal are not part of their public API, they are not observable from the outside world. Aiming at verifying such details is what you need to avoid as it leads to fragile unit tests.

When to use mocks: Inter-system communications

Inter-system communications

Inter-system communications is a different matter. Unlike collaborations between classes inside your application, the way your system talks to the outside world does comprise the public API of that system as a whole. It’s the contract, the postcondition it must hold at all times.

That distinction grows from the way separate applications evolve together. One of the main principles of such an evolution is maintaining backward compatibility. Regardless of the refactorings you perform inside your system, the communication pattern it uses to talk to external applications should stay in place, so that the external applications can understand it. For example, the messages your app emits on a bus should preserve their structure, the calls it issues to a payment gateway should have the same number and type of parameters, and so on.

That is where collaboration verification style of unit testing can be beneficial because it can be used to solve this exact problem – verify the communication pattern between your system and the external applications. The resulting tests in such situation don’t couple to implementation details because this type of communication is observable from the outside world and thus comprises part of the application’s public API.

Let’s take an example:

public class Order

{

    private List<Product> _products;

    private readonly IUser _user;

 

    public Order(IUser user)

    {

        _user = user;

    }

 

    public void AddProduct(Product product)

    {

        _products.Add(product);

        _user.UpdateLastBoughtProduct(product);

    }

}

This is a simple Order class which communicates with a user. When the client code adds a new product to the order, the class informs the user that it needs to update the last bought product.

And here’s the test that verifies that:

[Fact]

public void Test()

{

    var mock = new Mock<IUser>();

    var order = new Order(mock.Object);

    var product = new Product(“M0359”);

 

    order.AddProduct(product);

 

    mock.Verify(x => x.UpdateLastBoughtProduct(product));

}

It creates a mock for the IUser interface, supplies that mock to the order instance, and verifies that the UpdateLastBoughtProduct method was called.

This is an example of a fragile test. The communication between Order and User domain classes (the UpdateLastBoughtProduct method call) cannot be observed from the outside of the application and thus is not part of its public API. Verifying this communication, therefore, leads to coupling tests to implementation details which, in turn, cements those details and prevents you from refactoring them.

Here’s another example:

public class OrderService

{

    private readonly IPaymentGateway _gateway;

 

    public OrderService(IPaymentGateway gateway)

    {

        _gateway = gateway;

    }

 

    public void Submit(Order order)

    {

        _gateway.ChargePayment(order.TotalAmount);

    }

}

[Fact]

public void Test()

{

    var mock = new Mock<IPaymentGateway>();

    var order = new Order(100);

    var service = new OrderService(mock.Object);

 

    service.Submit(order);

 

    mock.Verify(x => x.ChargePayment(100m));

}

Here, the payment service is an external application and thus the call your system issues to that application is a side effect that is visible to the outside world. Coupling to that side effects doesn’t result in fragile tests. Unlike the example with User and Order, you do want to make sure this type of communication stays in place even after refactoring your code base, and the use of collaboration verification helps you do that.

Commands vs queries

The calls your system performs to external systems can be categorized further into commands and queries.

Adding a message to a message queue, sending an email, charging money through a payment service are all examples of commands: they change the state of applications called; produce side effects.

An example of a query would be a call to a location service in order to retrieve a ZIP code for a particular address, getting the latest stock quotes from a trading system and so on. In other words, any external call that doesn’t mutate the state of the callee is a query.

The difference between these types of external calls is that queries don’t comprise the API of your system, they are merely a means to get the information required to execute an operation. You still need to substitute them with test doubles because it’s often the only way to work with external dependencies but you shouldn’t verify that your application actually performs those calls.

Only commands constitute the application’s postconditions, and thus only commands should be backward compatible. Queries, on the other hand, can be re-arranged during refactorings or even completely removed, that shouldn’t be considered a bug by your tests.

Test doubles that are used merely to provide data for the SUT are called stubs. Here you can read more on the differences between them in the context of integration testing: Pragmatic integration testing.

When to use mocks: Summary

It’s important to differentiate inner- and inter-system communications. Collaboration verification (and, consequently, the use of mocks) is best when you need to validate communications between your and other applications. It brings damage when you try to employ it for communications inside your application.

  • Inner-system communications are implementation details.
  • Inter-system communications (the commands part of it) comprise the application’s API.
  • Use mocks and stubs only for inter-system communications.
  • External communications can be categorized into commands and queries.
  • Queries are not part of the application’s API and should not be verified in tests.

If you enjoyed this article, be sure to check out my Pragmatic Unit Testing Pluralsight course as well.

Related articles

Share




  • Amir Shitrit

    Great explanation and right on the spot (just today I had the exact same conversation with my architect).
    Regarding inter-system communication, do microservices introduce more of these boundaires?
    That is, perhaps interaction that used to be considered “inner” now becomes “inter”. Right?

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      Regarding inter-system communication, do microservices introduce more of these boundaires? That is, perhaps interaction that used to be considered “inner” now becomes “inter”. Right?

      Absolutely. And I personally don’t like too fine-grained microservice segregation because of that. If there’s not much logic in them, that’s fine. But if it’s something business-critical, I’d recommend to start with a bigger, well-designed monolith and split it only when you see multiple bounded contexts emerging in it.

  • kan

    It should be reasonably balanced. I could argue that in your example with UpdateLastBoughtProduct I see and easily understand the Order unit. In case if I want to refactor or change the unit, I don’t have to examine everything, including the User behaviour. I see a test here and I assume it is here for a reason, so I will keep it it green unless I see some conflicts with my changes.

    Your suggestions work only if the SUTs are small enough to be easily embraced ideally even by a developer which never seen it before.

  • Guillaume L

    Coupling to that side effects doesn’t result in fragile tests.

    This is interesting. Can you clarify why you think it is not the case here? Why does it not count as coupling tests to implementation details and what is the fundamental difference between external system gateway objects and internal objects that makes external system mocks harmless in tests?

    • http://enterprisecraftsmanship.com/ Vladimir Khorikov

      You’ll need to be more specific in your question. Right now, the only answer that comes to mind is the one I gave in the post: external communications are observable from the outside of the application and thus coupling to them will not result in fragile tests; those communications are not implementation details.

      • Guillaume L

        Okay, so are you saying that test fragility is not related to likelihood of breaking when something changes in the production code? Or is coupling to external things a special kind of coupling that is less fragile per se? If so, why? Just trying to understand.

        • http://enterprisecraftsmanship.com/ Vladimir Khorikov

          Or is coupling to external things a special kind of coupling that is less fragile per se?

          Yes.

          In my opinion, you get the most value out of your tests when you make them:
          1) Use the well-defined public API the SUT exposes. Tests shouldn’t try to invoke methods on the SUT which are not meant to be used by code from outside layers (such as private methods).
          2) Verify the end result of the SUT as it’s seen from the outside world.

          The closer tests get to acting like a normal client, the better because that reduces the number of false alarms during refactorings (decreases fragility).

          External calls fall into this definition: they are the end result the SUT produces which is visible from the outside of the SUT. Tests coupling to them are less fragile because those calls tend to stay in place during refactorings. As long as the requirements to your system don’t change, the kind of calls your application issues to external applications should also remain the same.

          • Guillaume L

            Thank you, that makes sense. Would you say that calls to a Repository or some sort of database fall into the same category?

          • http://enterprisecraftsmanship.com/ Vladimir Khorikov

            That’s actually gonna be the topic of a follow-up post. In short: I would say that it depends on how your application works with the database. If that DB is an application DB, meaning that it has only one client, then it isn’t an external application per se and it can be evolved together with the client application itself. Therefore, it makes sense to encapsulate the work with it within the bounded context (the application) and treat it as an implementation detail.

            So, answering your question, I wouldn’t say database calls fall into the same category in case of an application database. Such databases are not exposed to the outside world and thus not observable by clients directly. Database calls do fall into that category if that is an integration DB and your app is just one of many that works with it. The same is true for the work with the file system.