Welcome to our lesson on using Fakes as test doubles in Test Driven Development (TDD) with C#, xUnit
, and Moq
. In this lesson, you'll explore how fakes can streamline your testing by simulating real-world components. Our journey so far has exposed you to various test doubles like dummies, stubs, spies, and mocks. Now, we'll dive into fakes, which enable you to create realistic implementations that mirror complex dependencies, making your tests more robust and reliable. As always, we'll practice the TDD cycle: Red, Green, Refactor, as we see how fakes fit into our testing strategy.
Let's see how to implement a simple fake: an InMemoryUserRepository
. This serves as a stand-in for a real database repository, providing controlled behavior for our tests.
Create a class InMemoryUserRepository.cs
:
C#1using System; 2using System.Collections.Generic; 3using System.Linq; 4using System.Threading.Tasks; 5 6public class InMemoryUserRepository : IUserRepository 7{ 8 private readonly Dictionary<string, User> users = new(); 9 private int currentId = 1; 10 11 private string GenerateId() 12 { 13 return (currentId++).ToString(); 14 } 15 16 public async Task<User> Create(User userData) 17 { 18 var user = new User 19 { 20 Id = GenerateId(), 21 Email = userData.Email, 22 Name = userData.Name, 23 CreatedAt = DateTime.Now 24 }; 25 users[user.Id] = user; 26 return await Task.FromResult(user); 27 } 28 29 public async Task<User> FindById(string id) 30 { 31 users.TryGetValue(id, out var user); 32 return await Task.FromResult(user); 33 } 34 35 public async Task<User> FindByEmail(string email) 36 { 37 var user = users.Values.SingleOrDefault(u => u.Email == email); 38 return await Task.FromResult(user); 39 } 40 41 public async Task<User> Update(string id, User data) 42 { 43 if (!users.TryGetValue(id, out var existing)) 44 { 45 return await Task.FromResult<User>(null); 46 } 47 48 var updated = new User 49 { 50 Id = existing.Id, // Prevent id modification 51 Email = data.Email ?? existing.Email, 52 Name = data.Name ?? existing.Name, 53 CreatedAt = existing.CreatedAt // Prevent createdAt modification 54 }; 55 56 users[id] = updated; 57 return await Task.FromResult(updated); 58 } 59 60 public async Task<bool> Delete(string id) 61 { 62 return await Task.FromResult(users.Remove(id)); 63 } 64 65 public async Task<IList<User>> FindAll() 66 { 67 return await Task.FromResult(users.Values.ToList()); 68 } 69 70 public void Clear() 71 { 72 users.Clear(); 73 currentId = 1; 74 } 75}
Explanation:
- We create an in-memory store for users using a
Dictionary
. - Each function simulates typical database operations such as creating and finding users.
- The
Clear
method ensures data isolation between tests, a crucial feature for repeatable outcomes.
By having a controlled data store, we make sure our tests are focused on business logic and not dependent on an external database. Fakes are often quite complicated to build because they mimic the behavior of the real thing. They can be used to verify the state after your code acts on the fake, which can be very useful when you are trying to mimic the environment as best as possible without introducing the uncertainty or delay that the real implementation would introduce.
Next, we will use the fake repository to test a UserService
.
- Red: Write Failing Tests
Create a test file UserServiceTests.cs
:
C#1using System.Threading.Tasks; 2using Xunit; 3 4public class UserServiceTests 5{ 6 private UserService _userService; 7 private InMemoryUserRepository _userRepository; 8 9 public UserServiceTests() 10 { 11 // Arrange 12 _userRepository = new InMemoryUserRepository(); 13 _userService = new UserService(_userRepository); 14 } 15 16 [Fact] 17 public async void RegisterUser_ShouldCreateNewUserSuccessfully() 18 { 19 // Act 20 var user = await _userService.RegisterUser("test@example.com", "Test User"); 21 22 // Assert 23 Assert.Equal("test@example.com", user.Email); 24 Assert.Equal("Test User", user.Name); 25 Assert.NotNull(user.Id); 26 Assert.IsType<DateTime>(user.CreatedAt); 27 28 var repositoryUsers = await _userRepository.FindAll(); 29 Assert.Equal(1, repositoryUsers.Count); 30 Assert.Equal("Test User", repositoryUsers[0].Name); 31 } 32}
Run this test to confirm it fails, as we haven't implemented the logic yet.
- Green: Implement Minimal Code
Now, modify the UserService.cs
to ensure the test passes:
C#1public class UserService 2{ 3 private readonly IUserRepository _repository; 4 5 public UserService(IUserRepository repository) 6 { 7 this._repository = repository; 8 } 9 10 public async Task<User> RegisterUser(string email, string name) 11 { 12 return await this._repository.Create(new User { Email = email, Name = name }); 13 } 14}
Rerun the test. It should now pass, confirming our implementation meets the defined requirement.
In this lesson, we explored the implementation and use of fakes in TDD, specifically via an in-memory repository for user management. Remember the steps of TDD:
- Red: Write a test that fails first, setting clear goals for implementation.
- Green: Implement just enough code to make your test pass.
- Refactor: Improve code quality without altering functionality.
Leverage the practice exercises to reinforce these concepts with hands-on examples. Congratulations on navigating the complexities of testing with fakes; your commitment is paving the way for building efficient, scalable applications. This is the final lesson of the course, so kudos for reaching this milestone! Keep exploring and applying TDD principles in your projects.