An example of avoiding Mocks and Stubs in C# unit testing.

This article is an instalment in an article series about how to move from interaction-based testing to state-based testing. In the previous article, you saw an example of a pragmatic state-based test in F#. You can now take your new-found knowledge and apply it to the original C# example.

In the spirit of xUnit Test Patterns, in this article you'll see how to refactor the tests while keeping the implementation code constant.

The code shown in this article is available on GitHub.

Connect two users #

The previous article provides more details on the System Under Test (SUT), but here it is, repeated, for your convenience:

public class ConnectionsController : ApiController
{
    public ConnectionsController(
        IUserReader userReader,
        IUserRepository userRepository)
    {
        UserReader = userReader;
        UserRepository = userRepository;
    }
 
    public IUserReader UserReader { get; }
    public IUserRepository UserRepository { get; }
 
    public IHttpActionResult Post(string userId, string otherUserId)
    {
        var userRes = UserReader.Lookup(userId).SelectError(
            error => error.Accept(UserLookupError.Switch(
                onInvalidId: "Invalid user ID.",
                onNotFound:  "User not found.")));
        var otherUserRes = UserReader.Lookup(otherUserId).SelectError(
            error => error.Accept(UserLookupError.Switch(
                onInvalidId: "Invalid ID for other user.",
                onNotFound:  "Other user not found.")));
 
        var connect =
            from user in userRes
            from otherUser in otherUserRes
            select Connect(user, otherUser);
 
        return connect.SelectBoth(Ok, BadRequest).Bifold();
    }
 
    private User Connect(User user, User otherUser)
    {
        user.Connect(otherUser);
        UserRepository.Update(user);
 
        return otherUser;
    }
}

This implementation code is a simplification of the code example that serves as an example running through my two Clean Coders videos, Church Visitor and Preserved in translation.

A Fake database #

As in the previous article, you can define a test-specific Fake database:

public class FakeDB : Collection<User>, IUserReaderIUserRepository
{
    public IResult<UserIUserLookupError> Lookup(string id)
    {
        if (!(int.TryParse(id, out int i)))
            return Result.Error<UserIUserLookupError>(UserLookupError.InvalidId);
 
        var user = this.FirstOrDefault(u => u.Id == i);
        if (user == null)
            return Result.Error<UserIUserLookupError>(UserLookupError.NotFound);
 
        return Result.Success<UserIUserLookupError>(user);
    }
 
    public bool IsDirty { getset; }
 
    public void Update(User user)
    {
        IsDirty = true;
        if (!Contains(user))
            Add(user);
    }
}

This is one of the few cases where I find inheritance more convenient than composition. By deriving from Collection<User>, you don't have to explicitly write code to expose a Retrieval Interface. The entirety of a standard collection API is already available via the base class. Had this class been part of a public API, I'd be concerned that inheritance could introduce future breaking changes, but as part of a suite of unit tests, I hope that I've made the right decision.

Although you can derive a Fake database from a base class, you can still implement required interfaces - in this case IUserReader and IUserRepository. The Update method is the easiest one to implement, since it simply sets the IsDirty flag to true and adds the user if it's not already part of the collection.

The IsDirty flag is the only custom Retrieval Interface added to the FakeDB class. As the previous article explains, this flag provides a convenient was to verify whether or not the database has changed.

The Lookup method is a bit more involved, since it has to support all three outcomes implied by the protocol:

  • If the id is invalid, a result to that effect is returned.
  • If the user isn't found, a result to that effect is returned.
  • If the user with the requested id is found, then that user is returned.
This is a typical quality of a Fake: it contains some production-like behaviour, while still taking shortcuts compared to a full production implementation. In this case, it properly adheres to the protocol implied by the interface and protects its invariants. It still doesn't implement persistent storage, though.

Happy path test case #

This is all you need in terms of Test Doubles. You now have a test-specific IUserReader and IUserRepository implementation that you can pass to the Post method. Notice that a single class implements multiple interfaces. This is often key to be able to implement a Fake object in the first place.

Like in the previous article, you can start by exercising the happy path where a user successfully connects with another user:

[TheoryUserManagementTestConventions]
public void UsersSuccessfullyConnect(
    [Frozen(Matching.ImplementedInterfaces)]FakeDB db,
    User user,
    User otherUser,
    ConnectionsController sut)
{
    db.Add(user);
    db.Add(otherUser);
    db.IsDirty = false;
 
    var actual = sut.Post(user.Id.ToString(), otherUser.Id.ToString());
 
    var ok = Assert.IsAssignableFrom<OkNegotiatedContentResult<User>>(actual);
    Assert.Equal(otherUser, ok.Content);
    Assert.True(db.IsDirty);
    Assert.Contains(otherUser.Id, user.Connections);
}

This, and all other tests in this article use xUnit.net 2.3.1 and AutoFixture 4.1.0.

The test is organised according to my standard heuristic for formatting tests according to the Arrange Act Assert pattern. In the Arrange phase, it adds the two valid User objects to the Fake db and sets the IsDirty flag to false.

Setting the flag is necessary because this is object-oriented code, where objects have mutable state. In the previous articles with examples in F# and Haskell, the User types were immutable. Connecting two users didn't mutate one of the users, but rather returned a new User value, as this F# example demonstrates:

// User -> User -> User
let addConnection user otherUser =
    { user with ConnectedUsers = otherUser :: user.ConnectedUsers }

In the current object-oriented code base, however, connecting one user to another is an instance method on the User class that mutates its state:

public void Connect(User otherUser)
{
    connections.Add(otherUser.Id);
}

As a consequence, the Post method could, if someone made a mistake in its implementation, call user.Connect, but forget to invoke UserRepository.Update. Even if that happened, then all the other assertions would pass. This is the reason that you need the Assert.True(db.IsDirty) assertion in the Assert phase of the test.

While we can apply to object-oriented code what we've learned from functional programming, the latter remains simpler.

Error test cases #

While there's one happy path, there's four distinct error paths that you ought to cover. You can use the Fake database for that as well:

[TheoryUserManagementTestConventions]
public void UsersFailToConnectWhenUserIdIsInvalid(
    [Frozen(Matching.ImplementedInterfaces)]FakeDB db,
    string userId,
    User otherUser,
    ConnectionsController sut)
{
    Assert.False(int.TryParse(userId, out var _));
    db.Add(otherUser);
    db.IsDirty = false;
 
    var actual = sut.Post(userId, otherUser.Id.ToString());
 
    var err = Assert.IsAssignableFrom<BadRequestErrorMessageResult>(actual);
    Assert.Equal("Invalid user ID.", err.Message);
    Assert.False(db.IsDirty);
}
 
[TheoryUserManagementTestConventions]
public void UsersFailToConnectWhenOtherUserIdIsInvalid(
    [Frozen(Matching.ImplementedInterfaces)]FakeDB db,
    User user,
    string otherUserId,
    ConnectionsController sut)
{
    Assert.False(int.TryParse(otherUserId, out var _));
    db.Add(user);
    db.IsDirty = false;
 
    var actual = sut.Post(user.Id.ToString(), otherUserId);
 
    var err = Assert.IsAssignableFrom<BadRequestErrorMessageResult>(actual);
    Assert.Equal("Invalid ID for other user.", err.Message);
    Assert.False(db.IsDirty);
}
 
[TheoryUserManagementTestConventions]
public void UsersDoNotConnectWhenUserDoesNotExist(
    [Frozen(Matching.ImplementedInterfaces)]FakeDB db,
    int userId,
    User otherUser,
    ConnectionsController sut)
{
    db.Add(otherUser);
    db.IsDirty = false;
 
    var actual = sut.Post(userId.ToString(), otherUser.Id.ToString());
 
    var err = Assert.IsAssignableFrom<BadRequestErrorMessageResult>(actual);
    Assert.Equal("User not found.", err.Message);
    Assert.False(db.IsDirty);
}
 
[TheoryUserManagementTestConventions]
public void UsersDoNotConnectWhenOtherUserDoesNotExist(
    [Frozen(Matching.ImplementedInterfaces)]FakeDB db,
    User user,
    int otherUserId,
    ConnectionsController sut)
{
    db.Add(user);
    db.IsDirty = false;
 
    var actual = sut.Post(user.Id.ToString(), otherUserId.ToString());
 
    var err = Assert.IsAssignableFrom<BadRequestErrorMessageResult>(actual);
    Assert.Equal("Other user not found.", err.Message);
    Assert.False(db.IsDirty);
}

There's little to say about these tests that hasn't already been said in at least one of the previous articles. All tests inspect the state of the Fake database after calling the Post method. The exact interactions between Post and db aren't specified. Instead, these tests rely on setting up the initial state, exercising the SUT, and verifying the final state. These are all state-based tests that avoid over-specifying the interactions.

Specifically, none of these tests use Mocks and Stubs. In fact, at this incarnation of the test code, I was able to entirely remove the reference to Moq.

Summary #

The premise of Refactoring is that in order to be able to refactor, the "precondition is [...] solid tests". In reality, many development organisations have the opposite experience. When programmers attempt to make changes to how their code is organised, tests break. In xUnit Test Patterns this problem is called Fragile Tests, and the cause is often Overspecified Software. This means that tests are tightly coupled to implementation details of the SUT.

It's easy to inadvertently fall into this trap when you use Mocks and Stubs, even when you follow the rule of using Mocks for Commands and Stubs for Queries. Refactoring tests towards state-based testing with Fake objects, instead of interaction-based testing, could make test suites more robust to changes.

It's intriguing, though, that state-based testing is simpler in functional programming. In Haskell, you can simply write your tests in the State monad and compare the expected outcome to the actual outcome. Since state in Haskell is immutable, it's trivial to compare the expected with the actual state.

As soon as you introduce mutable state, structural equality is no longer safe, and instead you have to rely on other inspection mechanisms, such as the IsDirty flag seen in this, and the previous, article. This makes the tests slightly more brittle, because it tends to pull towards interaction-based testing.

While you can implement the State monad in both F# and C#, it's probably more pragmatic to express state-based tests using mutable state and the occasional IsDirty flag. As always, there's no panacea.

While this article concludes the series on moving towards state-based testing, I think that an appendix on Test Spies is in order.

Next: A pure Test Spy.


Comments

ladeak #

If we had checked the FakeDB contains to user (by retrieving, similar as in the F# case), and assert Connections property on the retrieved objects, would we still need the IsDirty flag? I think it would be good to create a couple of cases which demonstrates refactoring, and how overspecified tests break with the interaction based tests, while works nicely here.

2019-04-05 17:20 UTC

ladeak, thank you for writing. The IsDirty flag is essentially a hack to work around the mutable nature of the FakeDB. As the previous article describes:

"In the previous article, the Fake database was simply an immutable dictionary. This meant that tests could easily compare expected and actual values, since they were immutable. When you use a mutable object, like the above dictionary, this is harder. Instead, what I chose to do here was to introduce an IsDirty flag. This enables easy verification of whether or not the database changed."
The Haskell example demonstrates how no IsDirty flag is required, because you can simply compare the state before and after the SUT was exercised.

You could do something similar in C# or F#, but that would require you to take an immutable snapshot of the Fake database before exercising the SUT, and then compare that snapshot with the state of the Fake database after the SUT was exercised. This is definitely also doable (as the Haskell example demonstrates), but a bit more work, which is the (unprincipled, pragmatic) reason I instead chose to use an IsDirty flag.

Regarding more examples, I originally wrote another sample code base to support this talk. That sample code base contains examples that demonstrate how overspecified tests break even when you make small internal changes. I haven't yet, however, found a good home for that code base.

2019-04-06 10:25 UTC
Sven Grosen #

First of all, thank you for yet another awesome series.

The fragility of my various employers' unit tests has always bothered me, but I couldn't necessarily offer an alternative that reduced/removed that. After further thought, I initially came up with two objections to this approach (based on actual enterprise experience), but was easily able to dismiss them:

  1. What if the SUT has a lot of dependencies?
    • Then--following best practices--the SUT is doing too much
  2. What if the dependency has a lot of methods
    • Then--following best practices--the dependency is doing too much
The one point I wanted to seek clarification on though was that--as you spell out throughout the series--this "state-based" approach is no panacea and you may still need to do some test refactoring if you change the implementation of the SUT (e.g. if, referencing point #2 above, we break up a large dependency into a more targeted one). You may need to update your "fake" to account for that, but that that effort is much smaller than updating innumerable mock setup/verify calls, is that a correct summation? So I would have a "fake" per unit test fixture (to use nunit terminology) and would only need to update that one fake if/when the SUT for that fixture is refactored in such a way that impacts the fake.

After reading this series I was trying to imagine how I could introduce this into my team's codebase where both of the objections I listed above are very real problems (I am new to the team and trying to wrangle with these issues). I imagine a pragmatic first step would be to define multiple fakes for a given large SUT that attempt to group dependency behavior in some logical fashion. Per usual, you've given me a lot to think about and some motivation to clean up some code!

2019-12-12 15:24 UTC

Sven, thank you for writing. I think that your summary of my position is accurate. A Fake affords a 'one-stop' place where you can go and address changes in you SUT APIs. You'll still need to edit test code (your Fake implementation), but in single place.

We can, on the other hand, view multiple Setup/Verify changes as a violation of the DRY principle.

I don't understand, however, why you want to involve the concept of a Fixture, one way or another. A Fake is Fake, regardless of the Fixture in which it appears.

2019-12-12 19:47 UTC
Sven Grosen #

I don't understand, however, why you want to involve the concept of a Fixture, one way or another. A Fake is Fake, regardless of the Fixture in which it appears.
Mark, you are right and I had intended to remove that reference to fixtures but forgot to. I could easily see fakes living completely outside of any specific fixture.

2019-12-13 02:30 UTC


Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 01 April 2019 05:50:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 01 April 2019 05:50:00 UTC