DEV Community

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

Posted on

1

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!

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more →

Top comments (0)

nextjs tutorial video

Youtube Tutorial Series

So you built a Next.js app, but you need a clear view of the entire operation flow to be able to identify performance bottlenecks before you launch. But how do you get started? Get the essentials on tracing for Next.js from @nikolovlazar in this video series 👀

Watch the Youtube series

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay