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


129

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

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());
    }
}

1
Ilya가 제안한 것처럼 실제로 로깅 메서드 자체가 호출되었는지 테스트하지 않는 경우 모의를 스텁으로 사용할 수 있습니다. 이 경우 로거 조롱이 작동하지 않으며 몇 가지 다른 접근 방식을 시도 할 수 있습니다. 다양한 접근 방식을 보여주는 짧은 기사 를 작성했습니다 . 이 기사에는 각기 다른 옵션이있는 전체 GitHub 저장소가 포함되어 있습니다 . 결국, 필자는 ILogger <T> 유형으로 직접 작업하는 대신 자신의 어댑터를 사용하는 것이 좋습니다. 필요한 경우
ssmith

@ssmith가 언급했듯이 실제 호출을 확인하는 데 몇 가지 문제가 ILogger있습니다. 그는 그의 블로그 포스트에 좋은 제안을 가지고 있으며 아래 답변 에서 대부분의 문제를 해결하는 것 같은 내 솔루션을 제공했습니다 .
Ilya Chernomordik

답변:


141

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

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>();

5
디버그 출력 창에 기록하려면 팩토리에서 AddDebug ()를 호출합니다. var factory = serviceProvider.GetService <ILoggerFactory> (). AddDebug ();
spottedmahn

3
"진짜 로거"접근 방식이 더 효과적이라는 것을 알았습니다!
DanielV

1
실제 로거 부분은 특정 시나리오에서 LogConfiguration 및 LogLevel을 테스트하는데도 유용합니다.
Martin Lottering 2018

이 접근 방식은 스텁 만 허용하고 호출 확인은 허용하지 않습니다. 아래 답변 에서 대부분의 확인 문제를 해결하는 것처럼 보이는 솔루션을 제공했습니다 .
Ilya Chernomordik

102

실제로 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);
}   

이것은 .NET Core 1.1이 아닌 .NET Core 2.0에서만 작동하는 것 같습니다.
Thorkil Værge

3
@adospace은, 당신의 의견은 대답보다 훨씬 더 유용하다
조니 (5)

이것이 어떻게 작동하는지 예를 들어 줄 수 있습니까? 단위 테스트를 할 때 출력 창에 로그를 표시하고 싶습니다. 이것이 그렇게되는지 잘 모르겠습니다.
J86

@adospace 이것은 startup.cs에 들어가야합니까?
raklos

1
험 @raklos, 더 ServiceCollection가 인스턴스화 테스트 내부의 시작 방법에 사용되는 가정없는 것
adospace

32

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
  }
}

1
안녕하세요. 이것은 나를 위해 잘 작동합니다. 지금은 확인하거나 내 로그 정보를 볼 수있는 방법
말리크 saifullah을

VS에서 직접 단위 테스트 케이스를 실행하고 있습니다. 나는 그것에 대해 콘솔이 없습니다
말리크 saifullah

1
@maliksaifullah im resharper를 사용하고 있습니다. 내가 그와 대 확인하자
Jehof

1
@maliksaifullah VS의 TestExplorer는 테스트 출력을 여는 링크를 제공합니다. TestExplorer에서 테스트를 선택하면 하단에 링크가 있습니다
Jehof

1
감사합니다! 몇 가지 제안 : 1) type 매개 변수가 사용되지 않기 때문에 이것은 일반적 일 필요가 없습니다. 그냥 구현 ILogger하면 더 광범위하게 사용할 수 있습니다. 2) BeginScope실행 중에 스코프를 시작하고 종료하는 테스트 된 메서드가 로거를 폐기 할 것이므로는 자체적으로 반환되지 않아야합니다. 대신 구현하는 개인 "더미"중첩 클래스 생성 IDisposable(다음 제거 그의 인스턴스를 반환하고 IDisposable에서를 XunitLogger).
Tobias J

27

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);

당신은 내 하루를 구했습니다 .. 감사합니다.
KiddoDeveloper

15

내 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());

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


이것은 매우 우아한 솔루션입니다.
MichaelDotKnox

동의합니다.이 답변은 매우 좋았습니다. 그것은 많은 표를하지 않는 이유를 이해하지 않습니다
자드

1
Fab. 다음은 일반 버전이 아닙니다 ILogger. gist.github.com/timabell/d71ae82c6f3eaa5df26b147f9d3842eb
Tim Abell

LogWarning에서 전달한 문자열을 확인하기 위해 mock을 만들 수 있습니까? 예 :It.Is<string>(s => s.Equals("A parameter is empty!"))
Serhat

이것은 많은 도움이됩니다. 나에게 빠진 부분은 XUnit 출력에 쓰는 mock에서 콜백을 어떻게 설정할 수 있다는 것입니다. 나를 위해 콜백을 누르지 마십시오.
flipdoubt

6

다른 답변이 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


로거 호출을 확인하는 이유는 무엇입니까? 비즈니스 로직의 일부가 아닙니다. 나쁜 일이 발생하면 메시지를 로깅하는 것보다 실제 프로그램 동작 (예 : 오류 처리기 호출 또는 예외 발생)을 확인합니다.
Ilya Chumakov

1
글쎄요, 적어도 어떤 경우에는 그것을 테스트하는 것이 매우 중요하다고 생각합니다. 프로그램이 조용히 실패하는 것을 너무 많이 보았 기 때문에 예외가 발생했을 때 로깅이 발생했는지 확인하는 것이 합리적이라고 생각합니다. 예를 들어 "또는"이 아니라 실제 프로그램 동작과 로깅을 모두 테스트합니다.
Ilya Chernomordik

5

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

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입니다.


5

@ ivan-samygin 및 @stakx의 작업을 더욱 발전시켜 Exception 및 모든 로그 값 (KeyValuePairs)에서도 일치시킬 수있는 확장 메서드가 있습니다.

.Net Core 3, Moq 4.13.0 및 Microsoft.Extensions.Logging.Abstractions 3.1.0에서 작동합니다.

/// <summary>
/// Verifies that a Log call has been made, with the given LogLevel, Message and optional KeyValuePairs.
/// </summary>
/// <typeparam name="T">Type of the class for the logger.</typeparam>
/// <param name="loggerMock">The mocked logger class.</param>
/// <param name="expectedLogLevel">The LogLevel to verify.</param>
/// <param name="expectedMessage">The Message to verify.</param>
/// <param name="expectedValues">Zero or more KeyValuePairs to verify.</param>
public static void VerifyLog<T>(this Mock<ILogger<T>> loggerMock, LogLevel expectedLogLevel, string expectedMessage, params KeyValuePair<string, object>[] expectedValues)
{
    loggerMock.Verify(mock => mock.Log(
        expectedLogLevel,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((o, t) => MatchesLogValues(o, expectedMessage, expectedValues)),
        It.IsAny<Exception>(),
        It.IsAny<Func<object, Exception, string>>()
        )
    );
}

/// <summary>
/// Verifies that a Log call has been made, with LogLevel.Error, Message, given Exception and optional KeyValuePairs.
/// </summary>
/// <typeparam name="T">Type of the class for the logger.</typeparam>
/// <param name="loggerMock">The mocked logger class.</param>
/// <param name="expectedMessage">The Message to verify.</param>
/// <param name="expectedException">The Exception to verify.</param>
/// <param name="expectedValues">Zero or more KeyValuePairs to verify.</param>
public static void VerifyLog<T>(this Mock<ILogger<T>> loggerMock, string expectedMessage, Exception expectedException, params KeyValuePair<string, object>[] expectedValues)
{
    loggerMock.Verify(logger => logger.Log(
        LogLevel.Error,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((o, t) => MatchesLogValues(o, expectedMessage, expectedValues)),
        It.Is<Exception>(e => e == expectedException),
        It.Is<Func<It.IsAnyType, Exception, string>>((o, t) => true)
    ));
}

private static bool MatchesLogValues(object state, string expectedMessage, params KeyValuePair<string, object>[] expectedValues)
{
    const string messageKeyName = "{OriginalFormat}";

    var loggedValues = (IReadOnlyList<KeyValuePair<string, object>>)state;

    return loggedValues.Any(loggedValue => loggedValue.Key == messageKeyName && loggedValue.Value.ToString() == expectedMessage) &&
           expectedValues.All(expectedValue => loggedValues.Any(loggedValue => loggedValue.Key == expectedValue.Key && loggedValue.Value == expectedValue.Value));
}


1

단순히 더미를 만드는 것은 ILogger단위 테스트에 그다지 가치가 없습니다. 로깅 호출이 수행되었는지도 확인해야합니다. Moq 로 모의 ILogger를 삽입 할 수 있지만 호출을 확인하는 것은 약간 까다로울 수 있습니다. 이 기사 는 Moq를 사용한 검증에 대해 자세히 설명합니다.

다음은 기사의 매우 간단한 예입니다.

_loggerMock.Verify(l => l.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()), Times.Exactly(1));

정보 메시지가 기록되었는지 확인합니다. 그러나 메시지 템플릿 및 명명 된 속성과 같은 메시지에 대한 더 복잡한 정보를 확인하려면 더 까다로워집니다.

_loggerMock.Verify
(
    l => l.Log
    (
        //Check the severity level
        LogLevel.Error,
        //This may or may not be relevant to your scenario
        It.IsAny<EventId>(),
        //This is the magical Moq code that exposes internal log processing from the extension methods
        It.Is<It.IsAnyType>((state, t) =>
            //This confirms that the correct log message was sent to the logger. {OriginalFormat} should match the value passed to the logger
            //Note: messages should be retrieved from a service that will probably store the strings in a resource file
            CheckValue(state, LogTest.ErrorMessage, "{OriginalFormat}") &&
            //This confirms that an argument with a key of "recordId" was sent with the correct value
            //In Application Insights, this will turn up in Custom Dimensions
            CheckValue(state, recordId, nameof(recordId))
    ),
    //Confirm the exception type
    It.IsAny<NotImplementedException>(),
    //Accept any valid Func here. The Func is specified by the extension methods
    (Func<It.IsAnyType, Exception, string>)It.IsAny<object>()),
    //Make sure the message was logged the correct number of times
    Times.Exactly(1)
);

다른 모의 프레임 워크에서도 똑같이 할 수 있다고 확신하지만 ILogger인터페이스는 어렵습니다.


1
나는 그 정서에 동의하며, 당신이 말했듯이 표현을 만드는 것이 조금 어려울 수 있습니다. 나는 종종 같은 문제가 있었는데, 그래서 최근에 Moq.Contrib.ExpressionBuilders.Logging을 모아서 훨씬 더 맛있게 만드는 유창한 인터페이스를 제공했습니다.
rgvlee

1

여전히 실제라면. .net 코어> = 3에 대한 테스트에서 로그를 출력하는 간단한 방법

[Fact]
public void SomeTest()
{
    using var logFactory = LoggerFactory.Create(builder => builder.AddConsole());
    var logger = logFactory.CreateLogger<AccountController>();
    
    var controller = new SomeController(logger);

    var result = controller.SomeActionAsync(new Dto{ ... }).GetAwaiter().GetResult();
}

0

사용 Telerik 그냥 모의 로거의 조롱 인스턴스를 만들 수 있습니다 :

using Telerik.JustMock;
...
context = new XDbContext(Mock.Create<ILogger<XDbContext>>());

0

나는 NSubstitute를 사용하여 Logger 인터페이스를 모의하려고 시도했지만 ( Arg.Any<T>()제공 할 수없는 유형 매개 변수를 요구 하기 때문에 실패했습니다 ) 다음과 같은 방식으로 테스트 로거 (@jehof의 답변과 유사)를 생성했습니다.

    internal sealed class TestLogger<T> : ILogger<T>, IDisposable
    {
        private readonly List<LoggedMessage> _messages = new List<LoggedMessage>();

        public IReadOnlyList<LoggedMessage> Messages => _messages;

        public void Dispose()
        {
        }

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

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

        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
        {
            var message = formatter(state, exception);
            _messages.Add(new LoggedMessage(logLevel, eventId, exception, message));
        }

        public sealed class LoggedMessage
        {
            public LogLevel LogLevel { get; }
            public EventId EventId { get; }
            public Exception Exception { get; }
            public string Message { get; }

            public LoggedMessage(LogLevel logLevel, EventId eventId, Exception exception, string message)
            {
                LogLevel = logLevel;
                EventId = eventId;
                Exception = exception;
                Message = message;
            }
        }
    }

로깅 된 모든 메시지에 쉽게 액세스하고 함께 제공된 모든 의미있는 매개 변수를 주장 할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.