[C#] ASP.NET Core에서 ILogger로 단위 테스트하는 방법

이것은 내 컨트롤러입니다.

public class BlogController : Controller
{
    private IDAO<Blog> _blogDAO;
    private readonly ILogger<BlogController> _logger;

    public BlogController(ILogger<BlogController> logger, IDAO<Blog> blogDAO)
    {
        this._blogDAO = blogDAO;
        this._logger = logger;
    }
    public IActionResult Index()
    {
        var blogs = this._blogDAO.GetMany();
        this._logger.LogInformation("Index page say hello", new object[0]);
        return View(blogs);
    }
}

보시다시피 두 가지 종속성, a IDAO및 aILogger

그리고 이것은 내 테스트 클래스입니다. xUnit을 사용하여 테스트하고 Moq를 사용하여 mock 및 stub을 만듭니다. DAO쉽게 mock 할 수 있지만 ILogger어떻게해야할지 모르겠으므로 null을 전달하고 컨트롤러에 로그인하는 호출을 주석 처리합니다. 테스트를 실행할 때. 테스트하는 방법이 있지만 어떻게 든 로거를 유지합니까?

public class BlogControllerTest
{
    [Fact]
    public void Index_ReturnAViewResult_WithAListOfBlog()
    {
        var mockRepo = new Mock<IDAO<Blog>>();
        mockRepo.Setup(repo => repo.GetMany(null)).Returns(GetListBlog());
        var controller = new BlogController(null,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());
    }
}



답변

다른 종속성과 마찬가지로 조롱하십시오.

var mock = new Mock<ILogger<BlogController>>();
ILogger<BlogController> logger = mock.Object;

//or use this short equivalent 
logger = Mock.Of<ILogger<BlogController>>()

var controller = new BlogController(logger);

을 ( Microsoft.Extensions.Logging.Abstractions를) 사용 하려면 패키지를 설치해야 할 것입니다 ILogger<T>.

또한 실제 로거를 만들 수 있습니다.

var serviceProvider = new ServiceCollection()
    .AddLogging()
    .BuildServiceProvider();

var factory = serviceProvider.GetService<ILoggerFactory>();

var logger = factory.CreateLogger<BlogController>();


답변

실제로 Microsoft.Extensions.Logging.Abstractions.NullLogger<>완벽한 솔루션처럼 보이는 것을 찾았습니다 . 패키지를 설치 Microsoft.Extensions.Logging.Abstractions한 다음 예제에 따라 구성하고 사용합니다.

using Microsoft.Extensions.Logging;

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddSingleton<ILoggerFactory, NullLoggerFactory>();

    ...
}
using Microsoft.Extensions.Logging;

public class MyClass : IMyClass
{
    public const string ErrorMessageILoggerFactoryIsNull = "ILoggerFactory is null";

    private readonly ILogger<MyClass> logger;

    public MyClass(ILoggerFactory loggerFactory)
    {
        if (null == loggerFactory)
        {
            throw new ArgumentNullException(ErrorMessageILoggerFactoryIsNull, (Exception)null);
        }

        this.logger = loggerFactory.CreateLogger<MyClass>();
    }
}

및 단위 테스트

//using Microsoft.VisualStudio.TestTools.UnitTesting;
//using Microsoft.Extensions.Logging;

[TestMethod]
public void SampleTest()
{
    ILoggerFactory doesntDoMuch = new Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory();
    IMyClass testItem = new MyClass(doesntDoMuch);
    Assert.IsNotNull(testItem);
}   


답변

ITestOutputHelper(xunit에서)를 사용하여 출력 및 로그를 캡처 하는 사용자 정의 로거를 사용하십시오 . 다음은 state출력 에만을 쓰는 작은 샘플입니다 .

public class XunitLogger<T> : ILogger<T>, IDisposable
{
    private ITestOutputHelper _output;

    public XunitLogger(ITestOutputHelper output)
    {
        _output = output;
    }
    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        _output.WriteLine(state.ToString());
    }

    public bool IsEnabled(LogLevel logLevel)
    {
        return true;
    }

    public IDisposable BeginScope<TState>(TState state)
    {
        return this;
    }

    public void Dispose()
    {
    }
}

같은 단위 테스트에서 사용하십시오.

public class BlogControllerTest
{
  private XunitLogger<BlogController> _logger;

  public BlogControllerTest(ITestOutputHelper output){
    _logger = new XunitLogger<BlogController>(output);
  }

  [Fact]
  public void Index_ReturnAViewResult_WithAListOfBlog()
  {
    var mockRepo = new Mock<IDAO<Blog>>();
    mockRepo.Setup(repo => repo.GetMany(null)).Returns(GetListBlog());
    var controller = new BlogController(_logger,mockRepo.Object);
    // rest
  }
}


답변

Moq를 사용하는 .net 코어 3 답변의 경우

운 좋게도 stakx 는 좋은 해결 방법을 제공했습니다 . 그래서 다른 사람들을 위해 시간을 절약 할 수 있기를 바랍니다 (일을 파악하는 데 시간이 걸렸습니다).

 loggerMock.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>(),
                    (Func<It.IsAnyType, Exception, string>) It.IsAny<object>()),
                Times.Once);


답변

내 2 센트를 더하면 이것은 일반적으로 정적 도우미 클래스에 배치되는 도우미 확장 메서드입니다.

static class MockHelper
{
    public static ISetup<ILogger<T>> MockLog<T>(this Mock<ILogger<T>> logger, LogLevel level)
    {
        return logger.Setup(x => x.Log(level, It.IsAny<EventId>(), It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>()));
    }

    private static Expression<Action<ILogger<T>>> Verify<T>(LogLevel level)
    {
        return x => x.Log(level, 0, It.IsAny<object>(), It.IsAny<Exception>(), It.IsAny<Func<object, Exception, string>>());
    }

    public static void Verify<T>(this Mock<ILogger<T>> mock, LogLevel level, Times times)
    {
        mock.Verify(Verify<T>(level), times);
    }
}

그런 다음 다음과 같이 사용합니다.

//Arrange
var logger = new Mock<ILogger<YourClass>>();
logger.MockLog(LogLevel.Warning)

//Act

//Assert
logger.Verify(LogLevel.Warning, Times.Once());

물론 기대치 (예 : 기대치, 메시지 등)를 조롱하기 위해 쉽게 확장 할 수 있습니다.


답변

다른 답변이 mock을 전달하도록 제안하는 것처럼 쉽지만 ILogger실제로 logger에 대한 호출이 이루어 졌는지 확인하는 것이 갑자기 훨씬 더 문제가됩니다. 그 이유는 대부분의 호출이 실제로 ILogger인터페이스 자체에 속하지 않기 때문입니다 .

따라서 대부분의 호출은 Log인터페이스 의 유일한 메서드 를 호출하는 확장 메서드입니다 . 그 이유는 동일한 메서드로 귀결되는 오버로드가 하나만 있고 많지 않은 경우 인터페이스를 구현하는 것이 더 쉽다는 것입니다.

단점은 물론 확인해야하는 통화가 사용자가 만든 통화와 매우 다르기 때문에 통화가 이루어 졌는지 확인하는 것이 갑자기 훨씬 더 어렵다는 것입니다. 이 문제를 해결하기위한 몇 가지 다른 접근 방식이 있으며, 프레임 워크를 모의하는 사용자 지정 확장 메서드가 작성하기 가장 쉽다는 것을 발견했습니다.

다음은 작업하기 위해 만든 방법의 예입니다 NSubstitute.

public static class LoggerTestingExtensions
{
    public static void LogError(this ILogger logger, string message)
    {
        logger.Log(
            LogLevel.Error,
            0,
            Arg.Is<FormattedLogValues>(v => v.ToString() == message),
            Arg.Any<Exception>(),
            Arg.Any<Func<object, Exception, string>>());
    }

}

그리고 이것이 사용되는 방법입니다.

_logger.Received(1).LogError("Something bad happened");   

메서드를 직접 사용한 것과 똑같이 보이지만 여기서 트릭은 확장 메서드가 원래의 것보다 네임 스페이스에서 “가까워서”우선 순위가 부여되어 대신 사용된다는 것입니다.

불행히도 우리가 원하는 것을 100 % 제공하지 않습니다. 즉, 문자열을 직접 확인하지 않고 문자열을 포함하는 람다를 확인하기 때문에 오류 메시지가 좋지 않지만 95 %가없는 것보다 낫습니다. 🙂 또한 이 접근 방식은 테스트 코드를

PS For Moq Mock<ILogger<T>>Verify유사한 결과를 얻기 위해 확장 방법을 작성하는 접근 방식을 사용할 수 있습니다 .

PPS 이것은 .Net Core 3에서 더 이상 작동하지 않습니다. 자세한 내용은이 스레드를 확인하십시오 : https://github.com/nsubstitute/NSubstitute/issues/597#issuecomment-573742574


답변

이미 언급했듯이 다른 인터페이스처럼 조롱 할 수 있습니다.

var logger = new Mock<ILogger<QueuedHostedService>>();

여태까지는 그런대로 잘됐다.

좋은 점은 당신이 사용할 수 있다는 것입니다 Moq위해 특정 호출이 수행되었는지 확인합니다 . 예를 들어 여기에서 로그가 특정 Exception.

logger.Verify(m => m.Log(It.Is<LogLevel>(l => l == LogLevel.Information), 0,
            It.IsAny<object>(), It.IsAny<TaskCanceledException>(), It.IsAny<Func<object, Exception, string>>()));

Verify요점을 사용할 때 확장 메서드가 아닌 인터페이스 의 실제 Log메서드 에 대해 수행하는 것 ILooger입니다.