DEV Community

Cover image for Test with Dummy and Stub
Cesare De Sanctis
Cesare De Sanctis

Posted on

Test with Dummy and Stub

Let’s put theory into practice and start testing! In this article, we’ll focus on two types of Test Doubles — Dummy and Stub — to fully test our WeatherForecastController. Ready? Let’s dive in!

Dummy

As we can see from the previous article, the WeatherForecastController has two dependencies: the IWeatherServiceand the logger.

Although logging is important, we decided that "we don't care" about the logging part of the code. With this in mind, the logger becomes a good candidate for a Dummy since it is only needed for the Controller initialization, and we can't pass null to the constructor otherwise, at some point during the test, we would get an exception when the logger is called.

As you may remember from the first article of this series, a Dummy is an object that does nothing. The implementation of a Dummy for ILogger<WeatherForecastController> could look like this:

internal class DummyLogger<T> : ILogger<T>
{
    public IDisposable? BeginScope<TState>(TState state) where TState : notnull
        => null;

    public bool IsEnabled(LogLevel logLevel)
        => false;

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

Pretty easy, right? Just let Visual Studio implement the interface and return some default values where needed.

Can you spot the pattern? This is an implementation of the Null Object Pattern, which ensures that we don’t run into null reference errors during testing while keeping the code clean and focused on what we’re testing.


Tip: You can have a null logger for free using the NullLogger<T> in the Microsoft.Extensions.Logging.Abstractions namespace.


Stub

Now we need an implementation of the IWeatherService:

public interface IWeatherService
{
    IEnumerable<WeatherForecast> GetByCity(string city);
}
Enter fullscreen mode Exit fullscreen mode

with fixed response to predefined input.
Here we go:

internal class StubWeatherService : IWeatherService
{
    private readonly List<WeatherForecast> _weatherForecast = 
        [
            new WeatherForecast
            {
                Date = DateOnly.FromDateTime(DateTime.Now),
                TemperatureC = 20,
                Summary = "Test Summary"
            }
        ];

    public IEnumerable<WeatherForecast> GetByCity(string city)
        => string.Equals(city, "Rome") ? _weatherForecast : Enumerable.Empty<WeatherForecast>();
}
Enter fullscreen mode Exit fullscreen mode

With this implementation, we are sure that we can get a non-empty list when we pass "Rome" as the input to the GetByCity method.

Now we can create the WeatherForecastController using the Dummy logger and the Stub service. With this in place, unit testing the GET method becomes straightforward.

Tests

As a reminder, this is the method we want to test:

[HttpGet("{city}")]
public IActionResult Get(string city)
{
    var data = _weatherService.GetByCity(city);
    if (data.Any())
        return Ok(data);

    _logger.LogInformation("No data found for {City}", city);

    return NoContent();
}
Enter fullscreen mode Exit fullscreen mode

We know that "Rome" is the input for the GetByCity method to return some results, and we can make this information explicit in a variable in our test class, which looks like this:

public class TestsWithStub
{
    private readonly WeatherForecastController _sut;
    private readonly string _cityWithData = "Rome";

    public TestsWithStub()
    {
        _sut = new WeatherForecastController(
            weatherService: new StubWeatherService(),
            logger: new DummyLogger<WeatherForecastController>());
    }
}
Enter fullscreen mode Exit fullscreen mode

Following the code of the Controller, we want our first test to assert that we can get an Ok response with weather data when they are available, and we know they are for the input "Rome". Here's the code using XUnit:

[Fact]
public void Get_ReturnOk_When_Data_Exists()
{
    IActionResult actual = _sut.Get(_cityWithData);

    var okResult = Assert.IsType<OkObjectResult>(actual);
    var forecasts = Assert.IsAssignableFrom<IEnumerable<WeatherForecast>>(okResult.Value);
    Assert.NotEmpty(forecasts);
}
Enter fullscreen mode Exit fullscreen mode

We use Assert.IsType<OkObjectResult> to ensure the response type is correct, and Assert.IsAssignableFrom<IEnumerable<WeatherForecast>> to validate the returned object structure.

The second and last test will assert for a NoContent response when no data is available:

[Fact]
public void Get_ReturnNoContent_When_Data_NotExists()
{
    IActionResult actual = _sut.Get("Paris");

    Assert.IsType<NoContentResult>(actual);
}
Enter fullscreen mode Exit fullscreen mode

We have fully tested our WeatherForecastController!

What’s Next?

In the next article, we'll evolve the StubWeatherService, making it a Spy and exploring what we can do with it.

Stay tuned!

Top comments (0)