Replacing Mock and Stub with a Fake by Mark Seemann
A simple C# example.
A reader recently wrote me about my 2013 article Mocks for Commands, Stubs for Queries, commenting that the 'final' code looks suspect. Since it looks like the following, that's hardly an overstatement.
public User GetUser(int userId) { var u = this.userRepository.Read(userId); if (u.Id == 0) this.userRepository.Create(1234); return u; }
Can you spot what's wrong?
Missing test cases #
You might point out that this example seems to violate Command Query Separation, and probably other design principles as well. I agree that the example is a bit odd, but that's not what I have in mind.
The problem with the above example is that while it correctly calls the Read
method with the userId
parameter, it calls Create
with the hardcoded constant 1234
. It really ought to call Create
with userId
.
Does this mean that the technique that I described in 2013 is wrong? I don't think so. Rather, I left the code in a rather unhelpful state. What I had in mind with that article was the technique I called data flow verification. As soon as I had delivered that message, I was, according to my own goals, done. I wrapped up the article, leaving the code as shown above.
As the reader remarked, it's noteworthy that an article about better unit testing leaves the System Under Test (SUT) in an obviously defect state.
The short response is that at least one test case is missing. Since this was only demo code to show an example, the entire test suite is this:
public class SomeControllerTests { [Theory] [InlineData(1234)] [InlineData(9876)] public void GetUserReturnsCorrectValue(int userId) { var expected = new User(); var td = new Mock<IUserRepository>(); td.Setup(r => r.Read(userId)).Returns(expected); var sut = new SomeController(td.Object); var actual = sut.GetUser(userId); Assert.Equal(expected, actual); } [Fact] public void UserIsSavedIfItDoesNotExist() { var td = new Mock<IUserRepository>(); td.Setup(r => r.Read(1234)).Returns(new User { Id = 0 }); var sut = new SomeController(td.Object); sut.GetUser(1234); td.Verify(r => r.Create(1234)); } }
There are three test cases: Two for the parametrised GetUserReturnsCorrectValue
method and one test case for the UserIsSavedIfItDoesNotExist
test. Since the latter only verifies the hardcoded value 1234
the Devil's advocate can get by with using that hardcoded value as well.
Adding a test case #
The solution to that problem is simple enough. Add another test case by converting UserIsSavedIfItDoesNotExist
to a parametrised test:
[Theory] [InlineData(1234)] [InlineData(9876)] public void UserIsSavedIfItDoesNotExist(int userId) { var td = new Mock<IUserRepository>(); td.Setup(r => r.Read(userId)).Returns(new User { Id = 0 }); var sut = new SomeController(td.Object); sut.GetUser(userId); td.Verify(r => r.Create(userId)); }
There's no reason to edit the other test method; this should be enough to elicit a change to the SUT:
public User GetUser(int userId) { var u = this.userRepository.Read(userId); if (u.Id == 0) this.userRepository.Create(userId); return u; }
When you use Mocks (or, rather, Spies) and Stubs the Data Flow Verification technique is useful.
On the other hand, I no longer use Spies or Stubs since they tend to break encapsulation.
Fake #
These days, I tend to only model real application dependencies as Test Doubles, and when I do, I use Fakes.
While the article series From interaction-based to state-based testing goes into more details, I think that this small example is a good opportunity to demonstrate the technique.
The IUserRepository
interface is defined like this:
public interface IUserRepository { User Read(int userId); void Create(int userId); }
A typical Fake is an in-memory collection:
public sealed class FakeUserRepository : Collection<User>, IUserRepository { public void Create(int userId) { Add(new User { Id = userId }); } public User Read(int userId) { var user = this.SingleOrDefault(u => u.Id == userId); if (user == null) return new User { Id = 0 }; return user; } }
In my experience, they're typically easy to implement by inheriting from a collection base class. Such an object exhibits typical traits of a Fake object: It fulfils the implied contract, but it lacks some of the 'ilities'.
The contract of a Repository is typically that if you add an Entity, you'd expect to be able to retrieve it later. If the Repository offers a Delete
method (this one doesn't), you'd expect the deleted Entity to be gone, so that you can't retrieve it. And so on. The FakeUserRepository
class fulfils such a contract.
On the other hand, you'd also expect a proper Repository implementation to support more than that:
- You'd expect a proper implementation to persist data so that you can reboot or change computers without losing data.
- You'd expect a proper implementation to correctly handle multiple threads.
- You may expect a proper implementation to support ACID transactions.
The FakeUserRepository
does none of that, but in the context of a unit test, it doesn't matter. The data exists as long as the object exists, and that's until it goes out of scope. As long as a test needs the Repository, it remains in scope, and the data is there.
Likewise, each test runs in a single thread. Even when tests run in parallel, each test has its own Fake object, so there's no shared state. Therefore, even though FakeUserRepository
isn't thread-safe, it doesn't have to be.
Testing with the Fake #
You can now rewrite the tests to use FakeUserRepository
:
[Theory] [InlineData(1234)] [InlineData(9876)] public void GetUserReturnsCorrectValue(int userId) { var expected = new User { Id = userId }; var db = new FakeUserRepository { expected }; var sut = new SomeController(db); var actual = sut.GetUser(userId); Assert.Equal(expected, actual); } [Theory] [InlineData(1234)] [InlineData(9876)] public void UserIsSavedIfItDoesNotExist(int userId) { var db = new FakeUserRepository(); var sut = new SomeController(db); sut.GetUser(userId); Assert.Single(db, u => u.Id == userId); }
Instead of asking a Spy whether or not a particular method was called (which is an implementation detail), the UserIsSavedIfItDoesNotExist
test verifies the posterior state of the database.
Conclusion #
In my experience, using Fakes simplifies unit tests. While you may have to edit the Fake implementation from time to time, you edit that code in a single place. The alternative is to edit all affected tests, every time you change something about a dependency. This is also known as Shotgun Surgery and considered an antipattern.
The code base that accompanies my book Code That Fits in Your Head has more realistic examples of this technique, and much else.
Comments
Hi Mark,
Firstly, thank you for another insightful article.
I'm curious about using Fakes and testing exceptions. In scenarios where dynamic mocks (like Moq) are employed, we can mock a method to throw an exception, allowing us to test the expected behavior of the System Under Test (SUT). In your example, if we were using Moq, we could create a test to mock the UserRepository's Read method to throw a specific exception (e.g., SqlException). This way, we could ensure that the controller responds appropriately, perhaps with an internal server response. However, I'm unsure about how to achieve a similar test using Fakes. Is this type of test suitable for Fakes, or do such tests not align with the intended use of Fakes? Personally, I avoid using try-catch blocks in repositories or controllers and prefer handling exceptions in middleware (e.g., ErrorHandler). In such cases, I write separate unit tests for the middleware. Could this be a more fitting approach? Your guidance would be much appreciated.
(And yes, I remember your advice about framing questions —it's in your 'Code that Fits in Your Head' book! :D )
Thanks
Thank you for writing. That's a question that warrants an article or two. I've now published an article titled Error categories and category errors. It's not a direct answer to your question, but I found it useful to first outline my thinking on errors in general.
I'll post an update here when I also have an answer to your specific question.
AmirB, once again, thank you for writing. I've now published an article titled Testing exceptions that attempt to answer your question.