Best Practices for Unit Testing with ILogger in ASP.NET Core

Nov 23, 2025 · Programming · 24 views · 7.8

Keywords: Unit Testing | ILogger | ASP.NET Core | Moq | NullLogger

Abstract: This article explores three primary methods for unit testing controllers that use ILogger in ASP.NET Core applications: mocking ILogger with Moq, utilizing NullLogger for no-op logging, and verifying log calls with the Verify method. Through comprehensive code examples and in-depth analysis, it helps developers understand how to maintain logging functionality without compromising test performance, ensuring code quality and maintainability.

Introduction

In ASP.NET Core development, dependency injection and logging are core architectural patterns. However, when it comes to unit testing, handling dependencies like ILogger<T> can pose challenges. Many developers resort to passing null or commenting out log calls in tests, but this masks potential issues and reduces code coverage. This article provides effective strategies for testing code that includes ILogger, ensuring comprehensive and reliable tests.

Mocking ILogger with Moq Framework

Moq is a popular .NET mocking framework that can easily create mock instances of ILogger<T>. This approach allows control over logging behavior in tests without performing actual log operations. Below is a complete example demonstrating how to refactor test code to include a mocked ILogger:

using Moq;
using Microsoft.Extensions.Logging;

public class BlogControllerTest
{
    [Fact]
    public void Index_ReturnAViewResult_WithAListOfBlog()
    {
        var mockRepo = new Mock<IDAO<Blog>>();
        mockRepo.Setup(repo => repo.GetMany(null)).Returns(GetListBlog());
        
        var mockLogger = new Mock<ILogger<BlogController>>();
        ILogger<BlogController> logger = mockLogger.Object;
        
        var controller = new BlogController(logger, mockRepo.Object);
        
        var result = controller.Index();
        
        var viewResult = Assert.IsType<ViewResult>(result);
        var model = Assert.IsAssignableFrom<IEnumerable<Blog>>(viewResult.ViewData.Model);
        Assert.Equal(2, model.Count());
    }
}

In this example, Mock<ILogger<BlogController>> creates a mock logger that is passed to the controller constructor, ensuring the log dependency is properly injected without side effects in tests. Moq also offers a shorthand syntax, Mock.Of<ILogger<BlogController>>(), which can further simplify the code.

Utilizing NullLogger for No-Op Logging

For scenarios where log content verification is unnecessary, NullLogger provides a lightweight solution. It implements the ILogger interface but performs no operations in its log methods. This method is suitable when logging is not the focus of the test but dependency integrity must be maintained. Here is an example using NullLogger:

using Microsoft.Extensions.Logging.Abstractions;

public class BlogControllerTest
{
    [Fact]
    public void Index_ReturnAViewResult_WithAListOfBlog()
    {
        var mockRepo = new Mock<IDAO<Blog>>();
        mockRepo.Setup(repo => repo.GetMany(null)).Returns(GetListBlog());
        
        ILogger<BlogController> logger = new NullLogger<BlogController>();
        
        var controller = new BlogController(logger, mockRepo.Object);
        
        var result = controller.Index();
        
        var viewResult = Assert.IsType<ViewResult>(result);
        var model = Assert.IsAssignableFrom<IEnumerable<Blog>>(viewResult.ViewData.Model);
        Assert.Equal(2, model.Count());
    }
}

The advantage of NullLogger lies in its simplicity and performance, as it avoids any actual logging operations. However, it does not allow verification of log calls, making it ideal for tests where log content is irrelevant.

Verifying Log Calls

In some cases, verifying that logs are called correctly is a critical part of testing. Moq's Verify method enables checks on whether specific log methods were invoked and if the parameters match expectations. Below is an example using Verify to validate log calls:

using Moq;
using Microsoft.Extensions.Logging;

public class BlogControllerTest
{
    [Fact]
    public void Index_LogsInformation_WhenCalled()
    {
        var mockRepo = new Mock<IDAO<Blog>>();
        mockRepo.Setup(repo => repo.GetMany(null)).Returns(GetListBlog());
        
        var mockLogger = new Mock<ILogger<BlogController>>();
        ILogger<BlogController> logger = mockLogger.Object;
        
        var controller = new BlogController(logger, mockRepo.Object);
        
        var result = controller.Index();
        
        mockLogger.Verify(
            x => x.Log(
                LogLevel.Information,
                It.IsAny<EventId>(),
                It.Is<It.IsAnyType>((o, t) => string.Equals("Index page say hello", o.ToString(), StringComparison.InvariantCultureIgnoreCase)),
                It.IsAny<Exception>(),
                It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
            Times.Once);
    }
}

This test not only executes the controller's Index method but also verifies that the LogInformation method was called once with the expected message text. This approach ensures correct logging behavior, though it may add complexity to tests.

Summary and Best Practices

When selecting a unit testing strategy, weigh the methods based on test objectives. If logging is part of business logic, mocking with Moq and verifying calls is optimal. If logging is ancillary and does not affect core logic, NullLogger offers a cleaner solution. Regardless of the method, avoid passing null or commenting out log calls in tests to maintain code robustness and maintainability. By applying these techniques appropriately, developers can write comprehensive and efficient unit tests, enhancing overall software quality.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.