Aleks Vujic
Aleks Vujic

Reputation: 2269

Add additional method to service in unit tests

We are using .NET Core 3.1. We want to test whether correct messages are passed as an argument to our email service. Probably the way to go is to mock Send method and just save the message to in-memory queue/list. How can we do this without modifying the original interface (IEmailService)? We need one additional method in our mocked service which returns all the messages that were passed to its Send method, something like List<Message> GetSentEmails().

TestsBase.cs

public class TestsBase
{
    protected readonly IServiceScope _scope;
    protected readonly IPaymentService _paymentService;
    protected readonly IEmailService _emailService;
    
    public TestsBase(CustomWebApplicationFactory<Startup> factory)
    {
        _scope = factory.Services.CreateScope();
        _paymentService = _scope.ServiceProvider.GetService<IPaymentService>();
        _emailService = _scope.ServiceProvider.GetService<IEmailService>() as EmailServiceMock;
    }
}

[CollectionDefinition("MyCollection")]
public class MyCollection : ICollectionFixture<CustomWebApplicationFactory<Startup>>
{

}

PaymentServiceTest.cs

[Collection("MyCollection")]
public class PaymentServiceTest : TestsBase
{
    public PaymentServiceTest(CustomWebApplicationFactory<Startup> factory) : base(factory)
    {

    }
    
    [Fact]
    public void ConfirmPaymentTest()
    {
        // payment service also sends email
        _paymentService.Process(new Payment()
        {
            Amount = 203.12,
            Email = "[email protected]",
            ...
        });
        
        // we want to check if correct email was passed to email service
        var sentEmails = _emailService.GetSentEmails(); // HOW?
    }
}

CustomWebApplicationFactory.cs

public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // mock email service
            services.AddScoped<IEmailService, EmailServiceMock>();
        });
    }
}

EmailService.cs

public class EmailService : IEmailService
{
    // implementation
}

public class EmailServiceMock : IEmailService
{
    private readonly List<Message> _sentEmails;

    public EmailServiceMock()
    {
        _sentEmails = new List<Message>();
    }
    
    // mocked implementation of all the methods
    // but we also need access to _sentEmails
    
    public void Send(Message message)
    {
        _sentEmails.Add(message);
    }
}

Upvotes: 2

Views: 452

Answers (1)

rytisk
rytisk

Reputation: 1559

I'd suggest you use Moq library for this. You could mock your IEmailService and use Moq's Callback method to capture what arguments were passed to the Send method.

You would just need to tweak the initialization of your services.

[Fact]
public void ConfirmPaymentTest()
{
    var sentEmails = new List<Message>();
    var emailService = new Mock<IEmailService>();
    var paymentService = new PaymentService(emailService.Object);
    
    emailService
        .Setup(e => e.Send(It.IsAny<Message>()))
        .Callback<Message>(m => sentEmails.Add(m));   // instead of your GetSentEmails()
    
    paymentService.Process(new Payment()
    {
        Amount = 203.12,
        Email = "[email protected]",
        ...
    });
    
    // you can access your sentEmails list here
}

Moq is a really powerful library for testing .NET. There are more features that allow you to mock the return of a method, verify the count of how many times a method was invoked, etc.

Upvotes: 2

Related Questions