Introduction
In automated testing, there are numerous scenarios where we cannot properly test the System Under Test (SUT) due to dependencies on components that are unavailable, return values not useful for the test or when we simply desire more control over them. In these situations, we can use test doubles. A test double is an object used in testing the SUT that stands in for a real dependent component. It is not required to maintain the same functionality as the actual object, but it must provide the same API, as the SUT must be convinced it is interacting with a real dependent component. This article will cover two types of test doubles: Stubs and Mocks. For testing in the provided examples, the xUnit tool, Moq library and Fluent Assertions are used.
Stub
A stub object is used when our test depends on specific results returned by a component. In this case, we do not care how the method works; only the result is important. The stub bypasses the implementation of the method and returns a predefined value. This ensures that the outcome is not influenced by external parameters and always returns the expected value. Such test doubles are particularly useful in situations where the test is reliant on external APIs or requires data from a database, allowing for testing in a controlled environment without need for actual external dependencies.
Let us consider the code below:
// C# code
// Weather temperature interface
public interface IWeatherTemperature
{
public double GetTemperature(string city);
}
// Weather temperature service class
public class WeatherTemperatureService : IWeatherTemperature
{
// Get temperature in Celcius in the provided city
public double GetTemperature(string city)
{
//Return temperature in Celcius from the external API
return -10;
}
}
// Weather message service class
public class WeatherMessageService
{
private readonly IWeatherTemperature _weatherTemperatureService;
public WeatherMessageService(IWeatherTemperature weatherTemperatureService)
{
_weatherTemperatureService = weatherTemperatureService;
}
// Show message to user
public string ShowMessage(string city)
{
var temperatureInCelcius = _weatherTemperatureService.GetTemperature(city);
if (temperatureInCelcius <= 0)
{
return "It is freezing.";
}
else if (temperatureInCelcius < 15)
{
return "It is cold.";
}
else if (temperatureInCelcius < 25)
{
return "It is warm.";
} else
{
return "It is hot.";
}
}
} In the given example, there are a ‘WeatherTemperatureService’ and ‘WeatherMessageService’ classes. The ‘WeatherTemperatureService’ implements the ‘IWeatherTemperature’ interface. Its ‘GetTemperature’ method is designed to fetch temperature data from an external API. For simplification, this method’s implementation is not provided. An instance of the ‘WeatherTemperatureService’ class is utilized in the ‘WeatherMessageService’ through dependency injection. The ‘ShowMessage’ method within ‘WeatherMessageService’ then generates an appropriate message based on the temperature obtained from the API. Our goal is to test this particular method.
To gain control over the values returned by the ‘GetTemperature’ method, we can employ a stub. This will allow us to provide specific, expected data and then accurately predict the response from the ‘ShowMessage’ method.
// C# code
public class WeatherMessageServiceTests
{
[Theory]
[InlineData(-10, "It is freezing.")]
[InlineData(10, "It is cold.")]
[InlineData(20, "It is warm.")]
[InlineData(30, "It is hot.")]
public void CheckShowMessage_BasedOnGivenTemperature_ReturnsProperMessage(double temperatureInCelcius, string expectedMessage)
{
// Arrange
var weatherServiceStub = new Mock<IWeatherTemperature>();
weatherServiceStub
.Setup(m => m.GetTemperature(It.IsAny<string>()))
.Returns(temperatureInCelcius);
var weatherService = new WeatherMessageService(weatherServiceStub.Object);
// Act
var message = weatherService.ShowMessage("testCity");
// Assert
message.Should().Be(expectedMessage);
}
} The code above represents a `WeatherMessageService` test class, within which the `ShowMessage` method is tested. To focus our tests solely on the `ShowMessage` method, we avoid using the provided implementation of the `GetTemperature` method. Our goal is to test all possible message outcomes from the `ShowMessage` method. To achieve this, we create a stub that ensures the `GetTemperature` method returns expected values. Based on these values, we can predict the response from the `ShowMessage` method. The InlineData attribute supplies the temperatures and expected messages for all test cases. Utilizing the Moq library, the appropriate stub is created and passed to the `WeatherMessageService` constructor in the ‘Arrange’ section. Next, in the ‘Act’ section, we obtain the message. Finally, in the ‘Assert’ section, we compare the message from the `ShowMessage` method with the expected message provided in the InlineData attribute.
Stubs provide us with the capability to create tests that are independent of external conditions. This approach simplifies testing because it allows us to easily specify the values that will be returned by the method. Without stubs, the returned values could be influenced by external factors, potentially preventing us from covering all desired test cases.
Mock
The mock object, beyond returning predefined results from methods, is primarily responsible for monitoring interactions between components. This capability allows us to verify whether methods were called a specific number of times, in the correct order, with the correct parameters or to check if the data was processed as expected. Mocks enable us to compare our requirements with the actual behavior of the system under test.
In this context, let us focus on verifying that the `GetTemperature` method is called exactly once with the correct city name.
// C# code
public class WeatherMessageServiceTests
{
[Theory]
[InlineData(-10, "It is freezing.")]
[InlineData(10, "It is cold.")]
[InlineData(20, "It is warm.")]
[InlineData(30, "It is hot.")]
public void CheckShowMessage_BasedOnGivenTemperature_ReturnsProperMessage(double temperatureInCelcius, string expectedMessage)
{
// Arrange
var cityName = "testCity";
var weatherServiceMock = new Mock<IWeatherTemperature>();
weatherServiceMock
.Setup(m => m.GetTemperature(It.IsAny<string>()))
.Returns(temperatureInCelcius)
.Verifiable();
var weatherService = new WeatherMessageService(weatherServiceMock.Object);
// Act
var message = weatherService.ShowMessage(cityName);
// Assert
message.Should().Be(expectedMessage);
weatherServiceMock.Verify(mock => mock.GetTemperature(cityName), Times.Once());
}
} In the ‘Assert’ section’s final line, we perform a behavior verification of the `GetTemperature` method. This step confirms that `GetTemperature` was invoked precisely once with the specified city name. The ability to check method invocation details, including the number of times a method was called and with what arguments, is a feature provided by the Moq library. It is important to ensure that our components interact correctly with their dependencies under test conditions.
Mocks should be used in situations where, in addition to returning a predefined value from a method, it is also important to examine the behavior of that method. This approach provides us with greater control over the interactions with the test double, allowing for a more comprehensive understanding of how our system under test operates.
