DEV Community

Dennis
Dennis

Posted on

I stopped using mocking libraries, here's why

It seems like a no-brainer: When you create a new unit testing project, you install Moq or NSubstitute as well. How else are you going to fake your dependencies? You'd be crazy to write your own implementations, right? In this article, I hope to convince you that it's not as crazy as you might think.

Readability of my tests improved

This is easiest demonstrated with an example. Here is a traditional example using NSubstitute:

[Fact]
public void ShouldGetUserNameById()
{
    // given
    var userRepository = Substitute.For<IUserRepository>();
    userRepository.GetById(1234).Returns(new User(userName: "JohnDoe"));
    var userService = new UserService(userRepository);

    // when
    var result = userService.GetUserNameById(1234);

    // then
    Assert.Equal("JohnDoe", result);
}
Enter fullscreen mode Exit fullscreen mode

It's decent, but let's compare it with the same test using a custom fake:

[Fact]
public void ShouldGetUserNameById()
{
    // given
    var userRepository = new FakeUserRepository()
        .WithUser(id: 1234, userName: "JohnDoe");
    var userService = new UserService(userRepository);

    // when
    var result = userService.GetUserNameById(1234);

    // then
    Assert.Equal("JohnDoe", result);
}
Enter fullscreen mode Exit fullscreen mode

The test with the custom fake is much less "wordy". This may seem insignificant in a test like this, but once you get to your third test-double, the difference becomes noticeable.

Tests are more decoupled from implementations

You can see in the previous example that the version with with NSubstitute specifies the result for a single specific method. The example with custom test-double does not reference any specific method. This means that the test is only coupled to the signature of the test-double and not to the signature of the dependency itself. This makes the dependency more easy to change. To be more specific: When the signature of the dependency changes, I don't need to update all the tests that use it.

Additionally, I have more freedom in how to make my test pass. The version with NSubstitute relies on an implementation detail: the method call which I use to implement the requirement. In some cases, there may be multiple ways to achieve the same result and the mocking library forces you to choose which option to take beforehand. This may or may not be desirable.

I have a bunch of useful tools now

When you get used to writing your own test-doubles, you end up writing reusable tools that make testing easier and faster. I now have my own custom testing tools for:

  • Configuration using IOptionsMonitor<...>
  • Api requests and responses using HttpClient
  • IPublishedContent from Umbraco including modelsbuilder models, content navigation and URLs
  • TelemetryClient from Azure Application Insights

This has also led me to discover existing reusable testing tools for dates and times and logging. All of these tools have made my tests easier to read and easier to write.

TDD feels more natural

This is a more personal point about how I like to write my tests. I simply like to imagine how my test could look and just write that out. Often, my tests can't compile at first, because I made up function calls and types and properties that don't exist yet. This is particularly effective with AI assistants nowadays, which are very good at filling in the gaps. This is more difficult when I use mocking libraries, because I start thinking about how to write the test-double and forget to think about how the test reads. My mind shifts towards implementation before I'm done thinking about design.

Conclusion

Writing your own custom test-doubles is not as crazy as you may have thought. While mocking libraries are powerful tools, it should not be underestimated how much you can do with simple custom test-doubles. They improve the readability of tests and can decouple tests more from implementation.

Thank you for reading and I'll see you in my next blog! 😊

Top comments (0)