Lesson 5
Isolating Dependencies with Test Doubles: Fakes
Introduction to Fakes in TDD

Welcome to our lesson on using Fakes as test doubles in Test Driven Development (TDD) with Java, JUnit, and Mockito. 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.

Code Example and Walkthrough: Implementing an In-memory Fake Repository

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.java:

Java
1public class InMemoryUserRepository implements IUserRepository { 2 private final Map<String, User> users = new ConcurrentHashMap<>(); 3 private int currentId = 1; 4 5 private synchronized String generateId() { 6 return String.valueOf(currentId++); 7 } 8 9 @Override 10 public User create(User userData) { 11 User user = new User(); 12 user.setId(generateId()); 13 user.setEmail(userData.getEmail()); 14 user.setName(userData.getName()); 15 user.setCreatedAt(LocalDateTime.now()); 16 users.put(user.getId(), user); 17 return user; 18 } 19 20 @Override 21 public User findById(String id) { 22 return users.get(id); 23 } 24 25 @Override 26 public User findByEmail(String email) { 27 return users.values().stream() 28 .filter(u -> u.getEmail().equalsIgnoreCase(email)) 29 .findFirst() 30 .orElse(null); 31 } 32 33 @Override 34 public User update(String id, User data) { 35 User existing = users.get(id); 36 if (existing == null) { 37 return null; 38 } 39 40 if (data.getEmail() != null) { 41 existing.setEmail(data.getEmail()); 42 } 43 if (data.getName() != null) { 44 existing.setName(data.getName()); 45 } 46 // Prevent modification of id and createdAt 47 users.put(id, existing); 48 return existing; 49 } 50 51 @Override 52 public boolean delete(String id) { 53 return users.remove(id) != null; 54 } 55 56 @Override 57 public List<User> findAll() { 58 return new ArrayList<>(users.values()); 59 } 60 61 @Override 62 public void clear() { 63 users.clear(); 64 currentId = 1; 65 } 66}

Explanation:

  • We create an in-memory store for users using a ConcurrentHashMap.
  • 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 often mimic the behavior of real components, providing a safe and predictable testing environment without the complexity of external systems.

Building Tests Using the Fake Repository

Next, we will use the fake repository to test a UserService.

1 - Red: Write Failing Tests

Create a test file UserServiceTest.java:

Java
1public class UserServiceTest { 2 private UserService userService; 3 private InMemoryUserRepository userRepository; 4 5 @BeforeEach 6 public void setUp() { 7 // Arrange 8 userRepository = new InMemoryUserRepository(); 9 userService = new UserService(userRepository); 10 } 11 12 @Test 13 public void testRegisterUserCreatesNewUserSuccessfully() { 14 // Act 15 User user = userService.registerUser("test@example.com", "Test User"); 16 17 // Assert 18 assertEquals("test@example.com", user.getEmail()); 19 assertEquals("Test User", user.getName()); 20 assertNotNull(user.getId()); 21 assertNotNull(user.getCreatedAt()); 22 23 List<User> repositoryUsers = userRepository.findAll(); 24 assertEquals(1, repositoryUsers.size()); 25 assertEquals("Test User", repositoryUsers.get(0).getName()); 26 } 27}

Run this test to confirm it fails, as we haven't implemented the logic yet.

2 - Green: Implement Minimal Code

Now, modify the UserService.java to ensure the test passes:

Java
1public class UserService { 2 private final IUserRepository repository; 3 4 public UserService(IUserRepository repository) { 5 this.repository = repository; 6 } 7 8 public User registerUser(String email, String name) { 9 return this.repository.create(new User(email, name)); 10 } 11}

Rerun the test. It should now pass, confirming our implementation meets the defined requirement.

Review, Summary, and Preparation for Practice Exercises

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.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.