Table of Contents
Dependency injection and mocking
Tight coupling occurs when a class or component is heavily dependent on the specific implementation of another class, often creating instances of these dependencies directly within its code. While this may work for simple applications, tight coupling presents significant drawbacks in larger, more complex systems. Some of the main disadvantages of tight coupling include:
- Reduced Flexibility: When a class is tightly coupled to a specific dependency, it becomes difficult to swap out that dependency for an alternative implementation. For instance, if a class is tightly coupled to a specific database connection, replacing it with another type of database or a mock version for testing requires modifying the class itself, increasing the potential for errors.
- Challenging Testing: Tightly coupled code makes unit testing harder, as it’s difficult to isolate the class being tested. Since the dependent class is embedded, testing the code usually requires setting up the entire dependency as well, which can be time-consuming and may lead to complex, fragile tests.
- Higher Maintenance Costs: When classes are tightly coupled, any change in a dependency can force changes in multiple areas of the codebase, leading to a ripple effect that increases maintenance costs and the risk of introducing bugs. This tight interdependency can make the code harder to understand and maintain.
- Limited Reusability: Tightly coupled classes are often bound to specific implementations, making them less reusable in other parts of the application or in other projects. If a class depends on a specific instance of a service, it’s difficult to repurpose that class without also including the dependency.
To address these issues, Dependency Injection (DI) is introduced as a design pattern that helps decouple classes from their dependencies, improving flexibility, testability, and maintainability. With DI, instead of a class creating its own dependencies, those dependencies are provided to the class from an external source, typically through constructor parameters or method parameters. By externalising the creation and management of dependencies, DI allows for easy swapping, mocking, or replacing of dependencies without modifying the dependent class.
In practice, dependency injection works hand in hand with Inversion of Control (IoC), a principle in which the control of object creation and management is delegated to a container or framework. For example, in frameworks like ASP.NET Core, the DI container manages dependencies automatically, injecting them into classes as needed. This approach not only reduces tight coupling but also fosters a modular, testable architecture that is easier to maintain and adapt over time.
Fig. 1 illustrates the concept of dependency injection. Instead of creating a local instance of a dependency, a class makes use of an object such as a database connection that already exists in the context. The three main types of dependency injection are:
- Constructor injection: The service is provided as an argument to the client’s constructor.
- Setter injection: The client defines a setter function that can be used to set the value of an internal variable to an instance of the service
- Interface injection: The system context provides a framework for constructing service objects as needed and the client only needs a reference to the service’s interface. This is the most complex variation, but provides some additional benefits if the service needs to do additional work such as keeping track of the number of clients.
The example below (Wikipedia)
shows how a video game system could decouple gamepad functionality
from the rest of the system to allow for multiple implementations. Constructor injection
is used to provide the appropriate gamepad implementation to the GamePad
class at
instantiation.
using System;
namespace VideoGame;
interface IGamepadFunctionality {
string GetGamepadName();
GamePadSettings GetCurrentSettings();
...
}
class XBoxGamepad : IGamepadFunctionality {...}
class PlaystationJoystick : IGamepadFunctionality {...}
class SteamController : IGamepadFunctionality {...}
class Gamepad {
IGamepadFunctionality gamepadFunctionality;
public Gamepad(IGamepadFunctionality gamepadFunctionality) =>
this.gamepadFunctionality = gamepadFunctionality;
public GamePadSettings GetCurrentSettings() {
GamePadSettings gamePadSettings = new(
this.gamepadFunctionality.GetVibrationPower();
...
)
return gamePadSettings;
}
...
}
class Program {
static void Main() {
var steamController = new SteamController();
var gamepad = new Gamepad(steamController);
...
}
}
Notes
Lines 5-9: Define the interface implemented by each of the different gamepads
Line 16: The
GamePad
class defines an internal member that implements the gamepad interfaceLine 18: The internal member is set when the
GamePad
object is instantiated based on the argument to the constructorLine 34: The service is created as a singleton object in the main program
Mocking
In many situations, the code under test relies on some other system elements, either internal or external, that are either difficult or impossible to call directly. An example might be a method that calculates a result based on data from a database or from an external API. When testing the code, it is important to be able to control the input. However, it is a major chore to set up the required database records, and impossible to control the instantaneous output from an API that is under external control.
The solution is to use a mock object in place of the real element. A mock object stands in for the real one and is configured to behave in an entirely predictable way. Generally speaking, a mock is created either from a class definition or from an interface definition. Both provide information about the behaviour that is needed. Mocking a concrete class rather than an abstract interface has the benefit that the mock will preserve all the class behaviour other that the part we specifically want to control. However, Khorikov (2020) points out that the main reason that this appears to be necessary is a violation of the single responsibility principle. Ideally, the functionality that needs to be preserved should be decoupled from the behaviour that needs to be controlled. This would mean splitting the original class into parts, each with its own interface that can be mocked independently. Once the functionality has been partitioned, dependency injection can be used to provide the calling method with access to the dependent object.
Creating mocks
The general pattern for preparing a mock object consists of two steps. The first is to
create the mock, ideally from an interface definition. The second step is to define the
behaviour that is required for the test. Fig. 2 shows a code snippet that illustrates
this with an example using the moq library.
We assume that we are going to test a method DisplayCurrentGamepadSettings()
but we
do not want to be tied to using a physical gamepad. We therefore need a mock gamepad
object.
public void DisplayGamepadSettings_updates_ui_correctly()
{
var mockGamepad = new Mock<IGamepadFunctionality>();
mockGamepadSettings = new GamePadSettings();
mockGamepadSettings.vibrationPower = 0.5;
...
mockGamepad
.Setup(x => x.GetCurrentSEttings())
.Returns(mockGamepadSettings);
var gameDisplay = new GameDisplay(mockGamepad.Object);
gameDisplay.ShowGamepadSettings();
Assert.Equal(gameDisplay.GamepadPowerField.Text, mockGamepadSettings.vibrationPower.ToString());
}
1
2
3
4
5
6
7
8
9
10
public UserService(IUserRepository repository)
{
_repository = repository;
}
public string GetUserName(int id)
{
var user = _repository.GetUserById(id);
return user?.Name;
} }
public class UserServiceTests
{
[Fact]
public void GetUserName_ReturnsCorrectUserName()
{
// Arrange
var mockRepository = new Mock
1
2
3
4
5
6
7
8
9
var userService = new UserService(mockRepository.Object);
// Act
var result = userService.GetUserName(1);
// Assert
Assert.Equal("Alice", result);
mockRepository.Verify(repo => repo.GetUserById(1), Times.Once);
} } ```
In this example, Moq is used to create a mock IUserRepository
instance and to set up a response
for GetUserById
. We verify that the method is called once and that the result is as expected.
Moq’s fluent API simplifies the setup, verification, and control of the mock behaviour.
Mockito (Java)
In Java, Mockito is one of the most widely used mocking frameworks. It offers a fluent API, similar to Moq, and integrates well with JUnit, Java’s common unit testing framework.
Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class UserServiceTest {
@Test
void getUserName_ReturnsCorrectUserName() {
// Arrange
UserRepository mockRepository = mock(UserRepository.class);
User user = new User(1, "Alice");
when(mockRepository.getUserById(1)).thenReturn(user);
UserService userService = new UserService(mockRepository);
// Act
String result = userService.getUserName(1);
// Assert
assertEquals("Alice", result);
verify(mockRepository, times(1)).getUserById(1);
}
}
In this example, Mockito is used to mock the UserRepository interface. The when method allows us to specify that getUserById(1) should return a User object with the name “Alice”. The verify method checks that getUserById was called exactly once. Mockito’s syntax is clean and makes setting up expectations and verifications straightforward.
unittest.mock (Python)
Python’s unittest.mock is a built-in library for mocking, making it easily accessible for Python developers. It integrates well with Python’s unittest framework.
Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from unittest import TestCase
from unittest.mock import Mock
class UserService:
def __init__(self, repository):
self.repository = repository
def get_user_name(self, user_id):
user = self.repository.get_user_by_id(user_id)
return user['name'] if user else None
class TestUserService(TestCase):
def test_get_user_name(self):
# Arrange
mock_repository = Mock()
mock_repository.get_user_by_id.return_value = {'id': 1, 'name': 'Alice'}
user_service = UserService(mock_repository)
# Act
result = user_service.get_user_name(1)
# Assert
self.assertEqual(result, 'Alice')
mock_repository.get_user_by_id.assert_called_once_with(1)
In this example, unittest.mock is used to create a mock repository. The return_value attribute of get_user_by_id specifies the return data for this method. assert_called_once_with is used to verify that get_user_by_id was called with the expected argument.
Mocking frameworks are powerful tools that make unit testing more efficient and effective. By isolating dependencies, simplifying configuration, and providing verification capabilities, these frameworks enable developers to write robust, focused tests that improve code quality and reliability. Familiarity with mocking frameworks in your language of choice is an essential skill for building a solid testing foundation.