Lesson 5
Managing Dependencies in TDD with Java
Introduction to Managing Dependencies in TDD

In previous lessons, we've explored the fundamentals of Test Driven Development (TDD), specifically the Red-Green-Refactor cycle and the setup of a testing environment using Java and JUnit. Now, we shift our focus to a key aspect of TDD: managing dependencies. Managing dependencies ensures that each unit of your application can be tested in isolation, which is crucial in TDD for maintaining code reliability and robustness.

In this lesson, we will examine how to use interfaces for abstraction in Java, allowing us to manage dependencies effectively. Using simple examples, we will demonstrate how to apply the Red-Green-Refactor cycle in this context. We'll use Java with JUnit, a widely used framework in testing, to provide practical context. Let’s dive in.

Understanding Dependencies and Interfaces

Dependencies in software development refer to the components or systems that a piece of code relies on to function properly. In the context of testing, dependencies can complicate unit tests because they might introduce external factors that affect the test outcomes. To ensure tests are isolated and independent, we use abstractions.

An interface in Java acts as a contract that defines the methods a class should implement. By programming against interfaces, you can easily swap out implementations, making code more modular and test-friendly.

For example, consider a logger that a component uses to record actions. By abstracting the logger as an interface, you decouple the component from a specific logging implementation. This abstraction allows you to replace the actual logger with a mock or stub when testing, thus focusing on testing the component, not its dependencies.

Implementing Interfaces in Java

We'll create a simple logger interface called ILogger to demonstrate dependency management. This interface will define a method log, which our UserManager will use:

Java
1public interface ILogger { 2 void log(String message); 3}

The ILogger interface defines a single method log that accepts a message of type String. The simplicity of this interface highlights the ease of creating test stubs or mocks to simulate logging during tests without invoking an actual logging mechanism.

Building the UserManager with Dependency Injection

Next, we build a UserManager class by using the Logger interface. We utilize dependency injection to pass in a logger, illustrating how to maintain independence between the UserManager and any specific logging implementation.

Java
1public class UserManager { 2 private final ILogger logger; 3 private final List<String> users = new ArrayList<>(); 4 5 public UserManager(ILogger logger) { 6 this.logger = logger; 7 } 8 9 public void addUser(String username) { 10 users.add(username); 11 logger.log("User " + username + " added"); 12 } 13 14 public List<String> getUsers() { 15 return users; 16 } 17}

In UserManager, the logger is injected through the constructor, which allows you to provide different implementations of ILogger — such as a stub for testing or a real logger for production.

Testing with a Mock Logger

To test the UserManager without relying on a real logging mechanism, we create a MockLogger class. This mock implementation collects logged messages, enabling us to verify that the UserManager interacts with the ILogger as expected.

Java
1public class MockLogger implements ILogger { 2 private final List<String> logs = new ArrayList<>(); 3 4 @Override 5 public void log(String message) { 6 logs.add(message); 7 } 8 9 public List<String> getLogs() { 10 return logs; 11 } 12}

The MockLogger class implements the ILogger interface. It stores all log messages in a list, which can be accessed using the getLogs method. This makes it easy to verify if specific log messages were generated during the test.

Java
1public class UserManagerTest { 2 @Test 3 public void shouldAddUserSuccessfully() { 4 MockLogger mockLogger = new MockLogger(); 5 UserManager userManager = new UserManager(mockLogger); 6 7 userManager.addUser("john"); 8 9 assertTrue(userManager.getUsers().contains("john")); 10 } 11 12 @Test 13 public void shouldLogMessageWhenUserIsAdded() { 14 MockLogger mockLogger = new MockLogger(); 15 UserManager userManager = new UserManager(mockLogger); 16 17 userManager.addUser("john"); 18 assertEquals(List.of("User john added"), mockLogger.getLogs()); 19 } 20}

The UserManagerTest class includes two tests:

  1. shouldAddUserSuccessfully: Verifies that adding a user updates the internal user list correctly.
  2. shouldLogMessageWhenUserIsAdded: Confirms that the correct log message is generated when a user is added.

By using the MockLogger, we can ensure that the UserManager behaves correctly without relying on external logging systems, keeping the tests isolated and focused.

TDD Workflow in Action
  • Red: We start by writing tests for UserManager. The first test checks if a user is added correctly, while the second test verifies that logging occurs.
  • Green: Implement UserManager to pass these tests, ensuring that both the user addition and logging functionalities work as expected.
  • Refactor: The current implementation is effective, although always look for opportunities to improve code readability and maintainability.
Summary and Preparation for Hands-on Practice

In this lesson, we covered how to manage dependencies in unit testing using interfaces and dependency injection in Java. We explored the use of mock objects to isolate components during tests. Here's a recap of the key points:

  • Abstract dependencies using interfaces to facilitate test isolation.
  • Implement dependency injection to pass dependencies like loggers.
  • Use a mock logger to simulate dependencies in unit tests.
  • Always apply the TDD cycle: Red - Write a failing test, Green - Implement minimal code to pass, Refactor - Optimize the code without changing its functionality.

In the following hands-on practice sessions, you will consolidate these concepts by applying them using TDD. Continue practicing to deepen your understanding and proficiency in TDD with Java and effective dependency management.

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