Verifying collaborations at the system edges

Last week I wrote about when to use mocks. In this post, I’d like to outline a specific guideline which comes into play when you start working with mocks: verifying collaborations at the system edges. This article is based on my recent Pluralsight course about Pragmatic Unit Testing.

Verifying collaborations at the system edges

The use of mocks goes hand in hand with the style of unit testing which I call collaboration verification. Basically, that’s all there is to mocks: you use them when you want to make sure that some collaboration happens.

In the previous post, I wrote that the only valid use case for this style of unit testing is inter-system communications: when your application calls external applications, such as a message bus, and you need to affirm this communication takes place. For any other type of communication, it is better to check the actual result of it: the state the SUT ended up getting into after all collaborations complete.

However, it’s not enough to just use mocks whenever you need to verify how your system calls other systems. It’s also important to do that correctly. That is you need to do the verification at the very edges of your system.

Let’s take an example. Here’s an ESB gateway class:

public class EsbGateway
{
    private ISender _sender;
 
    public void SendUserCreatedMessage(int userId)
    {
        _sender.Send($"Subject: USER; Type: CREATED; Id: {userId}");
    }
 
    public void SendUserDeletedMessage(int userId)
    {
        _sender.Send($"Subject: USER; Type: DELETED; Id: {userId}");
    }
}

As you can see, it emits two types of messages on a bus. As I mentioned earlier, this bus is an example of an external application. Now, let’s say that you need to test how your code works with that bus. In other words, make sure correct messages come out as a result of some operation. How would you do that?

One way to implement such a test is to extract an interface out of the EsbGateway class, like this:

public interface IEsbGateway
{
    void SendUserCreatedMessage(int userId);
    void SendUserDeletedMessage(int userId);
}

And then use it in a test, like this one:

[Fact]
public void Test()
{
    var mock = new Mock<IEsbGateway>();
    var sut = new UserController(mock.Object);
 
    sut.CreateUser(123);
 
    mock.Verify(x => x.SendUserCreatedMessage(123));
}

There’s an issue with this code, however: it doesn’t follow the guideline I stated above. The ESB gateway does not reside at the edge of the application, it is merely an intermediate step on the message’s path from the internals of our system to the message bus. The type that does the actual work is the ISender interface, and so it’s that interface we need to substitute and verify in tests.

Here’s how we can depict the whole picture:

Processing pipeline
Processing pipeline

The event triggering the message originates in the controller. It uses the gateway to propagate that message to the message bus. However, it’s not the last element in the chain that leads to the external system. It is ISender that sits at the boundary between our application and the message bus.

So, what’s the problem with mocking an intermediate class, such as EsbGateway? Why not just use it instead of ISender? The issue here is that the further away you keep the mocks from the system edges, the more you expose your tests to potential false positives.

It’s basically the same problem as with mocking the internals of your application: the practice that causes the so-called design damage and leads to fragile tests. The way intermediate classes are organized have little effect on what messages are being sent eventually and therefore we shouldn’t couple tests to their internal workings. If we refactor the ESB gateway at some point in the future, all tests depending on it will break regardless of whether or not we change the actual messages emitted on the bus.

To avoid such a situation and to protect ourselves from false positives, we need to mock the very last component responsible for communicating with the outside world. Here’s how we can refactor the test you saw above:

[Fact]
public void Test()
{
    var mock = new Mock<ISender>();
    var esbGateway = new EsbGateway(mock.Object);
    var sut = new UserController(esbGateway);
 
    sut.CreateUser(123);
 
    mock.Verify(x => x.Send("Subject: USER; Type: CREATED; Id: 123"));
}

Now, compare it to the initial version of it:

[Fact]
public void Test()
{
    var mock = new Mock<IEsbGateway>();
    var sut = new UserController(mock.Object);
 
    sut.CreateUser(123);
 
    mock.Verify(x => x.SendUserCreatedMessage(123));
}

The string you see in the refactored version is exactly what is sent to the bus. If such test fails, this would be due to an actual change in the message format, and not just some unrelated refactoring made to an intermediate class. Overall, following this guideline helps with building more robust tests.

Emulating stimulus at the system edges

It’s often the case that along with verifying what messages your application sends to other applications, you also need to check how it reacts to messages coming in. The guideline here is basically the same: you need to emulate that stimulus at the very edges of your system.

For example, if you’ve got an incoming message from the bus, you might write a class analogous to ISender which instead of having a Send method would expose some delegate (or .NET event). You then could use that delegate to subscribe to the bus messages.

It’s a good idea to emulate an incoming message at this exact stage, where it enters the boundary of your system, and not in some intermediate component in the processing pipe. That would allow you to make sure you fully verify how your system reacts to such messages.

Note that all above is written in the context of integration testing: when you need to check how your application works in integration with other applications. That’s when you need to use mocks; regular unit tests that verify the internals of your app can and should be written without involving test doubles.

Summary

It’s important to push collaboration verifications as far to the system edges as possible. That will allow you to avoid test fragility which is usually associated with the use of mocks.

Similarly, if you need to emulate an incoming event, do that at the edges of your system as well. This will help you make sure your code base as a whole reacts to them properly.

Subscribe


I don't post everything on my blog. Don't miss smaller tips and updates. Sign up to my mailing list below.

Comments


comments powered by Disqus