Using Dependency Injection for Unit Testing with Mock Services

Avoiding Real External Calls by Replacing Dependencies with Mocks

Introduction

When writing unit tests, one of the biggest challenges is handling dependencies on external systems. For example, consider a UserService class that sends emails through an external EmailService. While this design works in production, it becomes problematic in a test environment. Running tests should be fast, reliable, and independent of outside systems. However, if the real EmailService is called, the test may send actual emails, consume unnecessary resources, and depend on network availability. To solve this issue, developers rely on dependency injection (DI) and mocking. Dependency injection allows us to pass a different implementation of a service into a class, while mocking provides a fake version of the external service that behaves like the original but does not perform real actions.

Master Python: 600+ Real Coding Interview Questions
Master Python: 600+ Real Coding Interview Questio

At its core, dependency injection means that a class should not create its own dependencies. Instead, dependencies should be provided externally, typically via the constructor or setter methods. In our case, UserService depends on EmailService. If UserService creates its own instance of EmailService, then it is tightly coupled to the real implementation, making testing difficult. But if we design UserService to accept an EmailService as a parameter, we can inject either the real service (in production) or a mock service (in tests).

Here’s a simple design:

public class UserService {
    private final EmailService emailService;

    // Constructor injection
    public UserService(EmailService emailService) {
        this.emailService = emailService;
    }

    public void registerUser(String userEmail) {
        // Business logic for user registration
        emailService.sendEmail(userEmail, "Welcome to our platform!");
    }
}

In the example above, UserService does not create its own EmailService. Instead, it receives one through the constructor. During production, we can inject the real service, but during testing, we inject a mock.

Now, when writing unit tests, instead of using the real service, we create a mock implementation of EmailService:

public class MockEmailService implements EmailService {
    private boolean emailSent = false;

    @Override
    public void sendEmail(String recipient, String message) {
        emailSent = true;  // Record that the method was called
    }

    public boolean isEmailSent() {
        return emailSent;
    }
}
Machine Learning & Data Science 600+ Real Interview Questions
Machine Learning & Data Science 600 Real Interview Questions

In the test:

@Test
public void testUserRegistrationSendsEmail() {
    MockEmailService mockEmailService = new MockEmailService();
    UserService userService = new UserService(mockEmailService);

    userService.registerUser("test@example.com");

    assertTrue(mockEmailService.isEmailSent());
}

Here, no real email is sent. The test simply checks whether the sendEmail method was called, ensuring that UserService behaves correctly. This is the essence of using dependency injection with mocks: isolating the class under test and verifying behavior without relying on external systems.

Many modern testing frameworks like Mockito in Java, unittest.mock in Python, or Moq in .NET simplify this process by automatically creating mocks. Instead of writing a MockEmailService, we can generate one at runtime and set expectations about how it should behave.



Master LLM and Gen AI: 600+ Real Interview Questions
Master LLM and Gen AI: 600+ Real Interview Questions

Conclusion

Dependency injection combined with mocking provides a clean and effective way to test classes like UserService that rely on external systems such as EmailService. By injecting dependencies instead of hardcoding them, we gain flexibility to swap real implementations with mocks in testing environments. This leads to faster, more reliable, and more maintainable tests, since they run independently of external services. Ultimately, adopting this approach helps ensure that unit tests remain true to their purpose: testing the logic of a class in isolation.

























Leave a Reply