Over-Reliance on Mocking Will Stall Your Unit Testing Efforts

by Hit Subscribe 13. December 2018 04:01

Mocks are a critical development tool. When you're testing a module that has external dependencies, the ability to imitate the outside world makes isolation possible. Mocking prevents unit tests from turning into automated integration tests. But if you rely too heavily on mocking in unit tests, you can stall or even foil your unit-testing efforts. Mocks are essential, but they're not always free or even cheap. Mocks add complexity.

Let's take a look at how.

Don_t_stall_your_unit_testing_efforts

Flexible Mocks Can Become Rigid Tests

Creating a mock means writing code that's specific to a test. You're replacing a dependency on the outside world with one in the code you've written. So what happens when the module under test changes?

Here's a simple test class.

First, we have a test fixture that creates the class under test and a single mock.

[TestFixture]
public class ManagerTest 
{
    Manager testInstance;
    Mock<Connection> myConnectionMock;

    [SetUp]
    public void SetUp()
    {
        myConnectionMock = new Mock<Connection>();
        testInstance = new Manager(myConnectionMock);
    }
}

Next, we have a single test method.

[Test]
public void DoTest()
{
    Widget myWidget = new Widget();
    myConnectionMock.Setup(m => m.SendWidget(ref myWidget)).Returns(true);

    var result = testInstance.SendTheWidget(myWidget);

    myConnectionMock.Verify((m => m.SendWidget()), Times.Once());
}

ManagerTest is a short test class, and refactoring a single test case doesn't sound like a very daunting task.

We're setting our precondition for myConnectionMock in the test method. This is a best practice for writing tests: the test has a clear arrange, act, and assert structure. So, if there's a change to the SendWidget method in Connection, refactoring the test is easy and intuitive.

But hopefully, your tests are more extensive than this example. In this case, we need at least two more; what if the widget is a null reference and what if the send fails? So, even a small change to our test dependency cascades into several changes in the tests.

None of this is a surprise. If you write code to pretend to be something and that something changes, your mock needs to change too. But, when you are overusing mocks a dependency change cascades into a set of broken tests. They often end up disabled because you don't have time to fix them.

Tests With Mocks Can Be Difficult to Understand

Some tests can be hard to understand before the refactoring begins. This is often a sign of writing code first and then trying to add tests after it's feature-complete.

Just looking at the test fixture gives you a sense of dread.

[TestFixture]
public class ManagerTest 
{
    Manager testInstance;
    Mock<Connection> myConnectionMock;
    Mock<Subscriber> mySubscriberMock;
    Mock<Widget> myWidgetMock;

    [SetUp]
    public void SetUp()
    {
        myConnectionMock = new Mock<Connection>();
        mySubscriberMock = new Mock<Subscriber>();
        myWidgetMock = new Mock<Widget>();
        testInstance = new Manager(myConnectionMock);
    }
}

The test method doesn't look any more encouraging.

[Test]
public void DoRetrieveTest()
{
    // Set up the mocks
    mySubscriberMock.Setup(m => m.RequestWidget("Widget 42")).Returns(myWidgetMock);
    myConnectionMock.Setup(m => m.GetSubscriber()).Returns(mySubscriberMock);
    myWidgetMock.Setup(m => m.GetName()).Returns("42");

    var result = testInstance.GetWidget("42");

    Assert.isTrue(result);

    // Verify that DoesSomething was called only once
    mySubscriberMock.Verify((m => m.RequestWidget("Widget 42")), Times.Once());
    myConnectionMock.Verify((m => m.GetSubscriber()), Times.Once());
    myWidgetMock.Verify((m => m.GetName()), Times.Once());
}

This test has a mock returning a mock that returns another mock. Hopefully, you've been luckier than I am and have never seen a test like this in real code. This class has three setup calls and three verification steps. It's a straightforward example of too much mocking.

What happens in a more complicated scenario? What if the Subscriber returns the same widget in a half-dozen tests? Maybe the developer moves that setup up to the SetUp method, so it's not repeated in every test method. This makes the code more concise, but we've broken the clear arrange-act-assert test flow.

Relying on too many mocks can make test code harder to understand. If you don't understand your tests, you're tempted to turn them off when they fail.

Mocks can Hide Design Problems

The structure of the second manager test implies that we have a violation of the single responsibility principle.

"A class should have only one reason to change." - Robert C. Martin

Manager owns a Connection, as it did on the first test. In that test, Manager looked like an abstraction layer that hid the inner workings of Connection from us. So, it's part of an API that supports more than one type of connection. That's a situation where judicious use of a mock makes sense.

But now, after Manager queries the Connection for a Widget, it evaluates what it receives and returns a result to the caller. It's responsible for retrieving the object and manipulating it. Manager might have to change when Connection, Subscriber, or Widget changes. It has too many responsibilities.

Did the questionable design lead to overuse of mocks, or is the overuse of mocks concealing a bad design? It doesn't matter. The point is that if you find yourself contemplating a test like this, or are handed a codebase that has one, it's time to consider a refactor.

A test like this will be hard to change when the underlying connection or subscriber changes. So what will happen when the refactoring has to happen quickly? The test will be disabled and fixed "when we get around to it."

Mocks Can Hide Behavior

The code snippets in this article are using the syntax for Moq, a popular .NET mocking framework. Moq's test doubles default to loose mode: if a test calls a method and does not have an expectation set, it will not throw and returns a sensible default value. If the mock is set to be strict, an unexpected method call results in an error.

Do you configure your mocks to be strict or loose? If you search for answers to this question you'll find people recommending that you stick with Moq's default loose mode. You'll also learn that most mocking libraries have the same default.

Why is that?

Let's look at an answer I've paraphrased from StackOverflow:

1. The tests become brittle—when you refactor the code the test often fails, even if what you are trying to test is still true.
2. The tests are harder to read—you need to have a setup for every method that is called on the mock.

In the first point, the recommendation is to not verify every interaction with a mock, because it might fail. This may be true. If you're mocking a third party library, you might not completely understand everything that happens "under the covers." Trying to create a strict mock might send you down a rabbit hole that has nothing to do with your test.

At the same time, choosing to ignore behavior might lead to problems later. It's possible that what you're overlooking now will become important in the future. Maybe the test should be failing.

The second point in the StackOverflow response reinforces what I said earlier. The fact that you're looking for a reason to avoid writing them is telling you something. Mocks can be hard to read and understand.

So When Should You Mock?

There's a time and a place for mocking. Tests without mocks can be slow since they might rely on external resources. They can also cross the line from unit tests to integration tests since unit tests shouldn't be relying on external resources anyway.

Robert Martin has a Goldilocks rule for mocking.

Mock across architecturally significant boundaries, but not within those boundaries.

In the examples above I was mocking a messaging system. While the second example was a bad one, they're both examples of when turning to a mock is a good choice. The mocks provide sound isolation and keep the test simple. It's also, as Martin says, an opportunity to think about how our code interacts with the external system.

Know When to Stop

Mocks are a valuable tool. They make it possible for us to isolate our code in tests that are clean and execute quickly without using an external system or dependencies. But, if we rely on them too heavily, they can cause more problems than they solve. Too many mocks can stall your testing efforts, rather than helping them.

This post was written by Eric Goebelbecker. Eric has worked in the financial markets in New York City for 25 years, developing infrastructure for market data and financial information exchange (FIX) protocol networks. He loves to talk about what makes teams effective (or not so effective!)

Tags:

Blog