유닛 테스트에서 로그 캡처

응용 프로그램 코드에서, 우리는 로그 문장을 작성하는 데 익숙하며, 주로 진단 목적에 사용된다.예를 들어, 우리는 로그를 사용하여 의외의 오류 흐름을 포착한다.따라서 단원 테스트에서 로그 출력을 포착하는 경우가 적지 않다.단원 테스트에서 로그 출력을 처리하는 데는 세 가지 독특한 옵션이 있는 것으로 알고 있습니다.

각본


우리의 테스트 장면은 문자열 입력을 받아들여 수정하지 않은 상태에서 되돌아오는 서비스나 테스트 시스템(SUT)이다.우리는 Microsoft Extensions에 의존하여 로그 기록을 진행한다.테스트 프레임워크로서 우리는 Xunit를 사용할 것이다.
public interface IEchoService
{
    Task<string> Echo(string input);
}

SUT의 초기 구현은 다음과 같습니다.
public class EchoService : IEchoService
{
    private readonly ILogger<EchoService> _logger;

    public EchoService(ILogger<EchoService> logger)
    {
        _logger = logger;
    }

    public Task<string> Echo(string input)
    {
        _logger.LogInformation("echo was invoked");
        return Task.FromResult(input);
    }
}

본문에 대해 말하자면, 위의 코드 단편은 충분하다.그러나 실제 응용에서 나는 입력을 기록하는 것을 더욱 좋아한다.그러나 만약 우리가 간단한 문자열 삽입값을 사용한다면, 우리는 즉시 그것에 대한 코드 분석 경고를 받을 것이다.여기서 권장 사항은 사용 가능high-performance logging한 LoggerMessage를 사용하는 것입니다.나는 항상 LoggerMessage 모드를 실현하려면 상당히 많은 템플릿 파일이 필요하다는 것을 발견했다.다행히도NET6, 이게 훨씬 쉬워요.우리는 generate 우리가 필요로 하는 모든 견본을 만들 수 있다.여느 때와 마찬가지로 Andrew Lock은 이미 이 새로운 기능에 관심을 가지기 시작했다.
우리 LoggerMessage 변경 사항을 SUT에 적용하면 다음 코드 세션처럼 보입니다.이 종류EchoService가 작동하도록 하기 위해서 그 자체가 partial로 표시되어 있음을 주의하십시오.
public partial class EchoService : IEchoService
{
    private readonly ILogger<EchoService> _logger;

    public EchoService(ILogger<EchoService> logger)
    {
        _logger = logger;
    }

    public Task<string> Echo(string input)
    {
        //_logger.LogInformation("echo was invoked");

        // The logging message template should not vary between calls to ... csharp(CA2254)
        // _logger.LogInformation($"echo was invoked with {input}");

        LogEchoCall(input);

        return Task.FromResult(input);
    }

    [LoggerMessage(1000, LogLevel.Information, "echo was invoked '{EchoInput}'")]
    partial void LogEchoCall(string echoInput);
}

옵션 1


우선 아무것도 하지 않는다.네, 잘 읽었어요.첫 번째 옵션을 nothing으로 시작하는 것은 어리석을 수도 있지만, 테스트 코드에서 로그 문장을 사용하지 않는 것은 충분히 가능하다.귀신이 곡할 노릇이다. 아무것도 하지 않아도 두 가지 맛이 난다.
"테스트에서 종속 주입을 사용하면""AddLogging()""에 액세스할 수 있습니다."만약 우리가 로그 제공 프로그램을 제공하지 않는다면, 우리의 코드는 정상적으로 실행될 수 있을 것이다.그렇지 않으면 로그 공급자를 설정하거나 명확하게 제공하면 현재 설정에 따라 0 개 이상의 공급자에 로그인합니다.예를 들어, 테스트 중에 Console Logger Provider를 사용하여 콘솔에 로그인할 수 있습니다.나는 IService Collection에서 확장 방법을 작성해서 코드를 작성하기 때문에 테스트 코드에서 같은 확장 방법을 사용하면 문제를 간소화할 수 있다.
[Fact]
public async Task Test_DependencyInjection_EmptyLoggingBuilder()
{
    var configuration = new ConfigurationBuilder().Build();
    var serviceProvider = new ServiceCollection()
        .AddLogging() // could also be part of AddEcho to make sure ILogger is available outside ASP.NET runtime
        .AddEcho(configuration)
        .BuildServiceProvider();
    var sut = serviceProvider.GetRequiredService<IEchoService>();
    var testInput = "Scenario: empty logging builder";
    var testResult = await sut.Echo(testInput).ConfigureAwait(false);
    testResult.Should().Be(testInput, "the input should have been returned");
}


[Fact]
public async Task Test_DependencyInjection_ConsoleLoggingBuilder()
{
    var configuration = new ConfigurationBuilder().Build();
    var serviceProvider = new ServiceCollection()
        .AddLogging(loggingBuilder => {
            loggingBuilder.AddConsole();
        })
        .AddEcho(configuration)
        .BuildServiceProvider();
    var sut = serviceProvider.GetRequiredService<IEchoService>();
    var testInput = "Scenario: console logging builder";
    var testResult = await sut.Echo(testInput).ConfigureAwait(false);
    testResult.Should().Be(testInput, "the input should have been returned");
}


단, 테스트에서 의존항 주입에 의존할 수 없으면 수동으로 SUT와 관련 의존항을 만들 수 있습니다.EchoService의 유일한 종속성은 Ilogger의 인스턴스입니다.테스트 목적으로 NullLoggerFactory를 사용할 수 있습니다. 로그 기록기를 만들어서void에 로그인합니다.
[Fact]
public async Task Test_Manuel_NullLoggingFactory()
{
    var sut = new EchoService(NullLogger<EchoService>.Instance);
    var testInput = "Scenario: null logger factory";
    var testResult = await sut.Echo(testInput).ConfigureAwait(false);
    testResult.Should().Be(testInput, "the input should have been returned");
}


As you can see in the screenshot above, and empty logger and a NullLogger are not the same thing.


선택 2


두 번째 방법은 Moq 프레임워크를 사용합니다. 이로써 기록기를 Mock 뒤에 숨길 수 있습니다. 이것은 Ilogger의 가짜 버전이라는 것을 의미합니다.나의 이전 문장에서, 나는 내가 좋아하는 모의 작성 방법을 이야기했다.나는 심지어 LoggerMock의 초기 버전도 포함했다.그 이후로 나는 이 개념을 더욱 충실하게 하기 때문에 여기는 Logger Mock의 업데이트 버전이다.
public class LoggerMock<TCategoryName> : Mock<ILogger<TCategoryName>>
{
    private readonly List<LogMessage> logMessages = new();

    public ReadOnlyCollection<LogMessage> LogMessages => new(logMessages);

    protected LoggerMock()
    {
    }

    public static LoggerMock<TCategoryName> CreateDefault()
    {
        return new LoggerMock<TCategoryName>()
            .SetupLog()
            .SetupIsEnabled(LogLevel.Information);
    }

    public LoggerMock<TCategoryName> SetupIsEnabled(LogLevel logLevel, bool enabled = true)
    {
        Setup(x => x.IsEnabled(It.Is<LogLevel>(p => p.Equals(logLevel))))
            .Returns(enabled);
        return this;
    }

    public LoggerMock<TCategoryName> SetupLog()
    {
        Setup(logger => logger.Log(
            It.IsAny<LogLevel>(),
            It.IsAny<EventId>(),
            It.Is<It.IsAnyType>((v, t) => true),
            It.IsAny<Exception>(),
            It.Is<Func<It.IsAnyType, Exception?, string>>((v, t) => true)
        ))
        .Callback(new InvocationAction(invocation => {
            var logLevel = (LogLevel)invocation.Arguments[0];
            var eventId = (EventId)invocation.Arguments[1];
            var state = invocation.Arguments[2];
            var exception = (Exception?)invocation.Arguments[3];
            var formatter = invocation.Arguments[4];

            var invokeMethod = formatter.GetType().GetMethod("Invoke");
            var actualMessage = (string?)invokeMethod?.Invoke(formatter, new[] { state, exception });

            logMessages.Add(new LogMessage {
                EventId = eventId,
                LogLevel = logLevel,
                Message = actualMessage,
                Exception = exception,
                State = state
            });
        }));
        return this;
    }
}

Moq를 사용하여 만든 모든 Mock 는 아날로그 클래스에 대한 호출을 단언할 수 있도록 합니다.나의 방법은 시뮬레이션을 상태가 있게 하기 때문에, 나는 그것에 대한 어떠한 요구도 포착할 수 있다.우리는 EventIdLogLevel 등의 정보를 방문할 수 있기 때문에 구체적인 단언을 할 수 있다.예를 들어 업무 이벤트에 대한 경보가 있으면 로그 시스템에 정확한 정보를 전달했는지 확인해야 한다.
[Fact]
public async Task Test_Moq_DefaultMockedLogger()
{
    var loggerMock = LoggerMock<EchoService>.CreateDefault();
    var sut = new EchoService(loggerMock.Object);
    var testInput = "Scenario: mocked logger";
    var testResult = await sut.Echo(testInput).ConfigureAwait(false);
    testResult.Should().Be(testInput, "the input should have been returned");

    loggerMock.LogMessages.Should().NotBeEmpty().And.HaveCount(1);
    loggerMock.VerifyEventWasLogged(new EventId(1000));
}

[Fact]
public async Task Test_Moq_LogLevelDisabledMockedLogger()
{
    var loggerMock = LoggerMock<EchoService>.CreateDefault().SetupIsEnabled(LogLevel.Information, enabled: false);
    var sut = new EchoService(loggerMock.Object);
    var testInput = "Scenario: log level disabled mocked logger";
    var testResult = await sut.Echo(testInput).ConfigureAwait(false);
    testResult.Should().Be(testInput, "the input should have been returned");

    loggerMock.LogMessages.Should().BeEmpty();
}


옵션3


지금까지 외부에서 유효한 옵션Xunit에 대해 논의했습니다.세 번째 기술은 Xunit에 국한되지 않지만 Xunit 프로젝트에서만 사용할 수 있다. 왜냐하면 우리는 현재 Xunit의 ITestOutputHelper 메커니즘에 의존하기 때문이다.대부분의 경우 우리는 ITestOutputHelper를 사용하여 테스트 용례 자체 내부의 줄을 기록한다.그러나, 쓰기 ILogger 를 만들 수 있습니다. ITestOutputHelper 그러면 SUT에서 생성한 로그를 포착할 수 있습니다.
마이크로소프트well-written documentation는 사용자 정의 기록기 제공 프로그램을 어떻게 만드는지 토론했다.우리는 우리의 XunitLogger 설정 클래스부터 시작한다.이 프레젠테이션에서, 우리는 사용자 정의 설정이 없지만, 설정을 적당한 위치에 두면 나중에 설정을 추가하기가 더욱 쉽다.예를 들어, ConsoleLogger 구성을 사용하여 LogScope 포함 및 타임스탬프 형식을 제어합니다.
public class XunitLoggerConfiguration
{
}

다음은 Xunit 레코더 자체입니다.문서의 ColoredConsole 예는 범위에 아무런 영향을 주지 않지만, 앞으로 우리 자신을 제한하지 않기 위해서, 우리는 BeginScope의 실현을 사용IExternalScopeProvider으로 변경할 것입니다.로그 줄을 인쇄하려면 포맷 프로그램 Log<TState> 의 마지막 인자가 필요합니다.그런 다음 Xunit의 Itest OutputHelpercapture output에 전달합니다.구체적인 요구에 따라 기록기의 종류 (이름), 이벤트, 로그 단계, 범위, 심지어 이상을 기록할 수 있습니다.이제 간단하게 하자.
public class XunitLogger : ILogger
{
    private readonly string _loggerName;
    private readonly Func<XunitLoggerConfiguration> _getCurrentConfig;
    private readonly IExternalScopeProvider _externalScopeProvider;
    private readonly ITestOutputHelper _testOutputHelper;

    public XunitLogger(string loggerName, Func<XunitLoggerConfiguration> getCurrentConfig, IExternalScopeProvider externalScopeProvider, ITestOutputHelper testOutputHelper)
    {
        _loggerName = loggerName;
        _getCurrentConfig = getCurrentConfig;
        _externalScopeProvider = externalScopeProvider;
        _testOutputHelper = testOutputHelper;
    }

    public IDisposable BeginScope<TState>(TState state) => _externalScopeProvider.Push(state);

    public bool IsEnabled(LogLevel logLevel) => LogLevel.None != logLevel;

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
    {
        if (!IsEnabled(logLevel))
        {
            return;
        }

         var message = formatter(state, exception);
         _testOutputHelper.WriteLine(message);
    }
}

AnILoggerProviderILogger개의 실례를 창설하는 것을 책임진다.이것은 우리가 우리의 XunitLoggerProvider를 맞춤형으로 만들어야 한다는 것을 의미한다.
public sealed class XunitLoggerProvider : ILoggerProvider
{
    private readonly IDisposable _configurationOnChangeToken;
    private XunitLoggerConfiguration _currentConfiguration;
    private readonly ConcurrentDictionary<string, XunitLogger> _loggers = new();
    private readonly IExternalScopeProvider _externalScopeProvider = new LoggerExternalScopeProvider();
    private readonly ITestOutputHelper _testOutputHelper;

    public XunitLoggerProvider(IOptionsMonitor<XunitLoggerConfiguration> optionsMonitor, ITestOutputHelper testOutputHelper)
    {
        _currentConfiguration = optionsMonitor.CurrentValue;
        _configurationOnChangeToken = optionsMonitor.OnChange(updatedConfiguration => _currentConfiguration = updatedConfiguration);
        _testOutputHelper = testOutputHelper;
    }

    public ILogger CreateLogger(string categoryName)
    {
        var logger = _loggers.GetOrAdd(categoryName, name => new XunitLogger(name, GetCurrentConfiguration, _externalScopeProvider, _testOutputHelper));
        return logger;
    }

    public void Dispose()
    {
        _loggers.Clear();
        _configurationOnChangeToken.Dispose();
    }

    private XunitLoggerConfiguration GetCurrentConfiguration() => _currentConfiguration;
}

마지막 문제는 우리가 새로운 기록기 유형을 등록할 수 있도록 확장하는 것이다.LoggingBuilder의 DI 용기에도 XunitLogger를 추가합니다.이것이 바로 앞의 코드 세그먼트 ITestOutputHelper 에서 의존항 주입 용기에서 그것을 검색할 수 있는 이유입니다.
public static class XunitLoggingBuilderExtensions
{
    public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper testOutputHelper)
    {
        builder.AddConfiguration();

        builder.Services.TryAddSingleton(testOutputHelper);

        builder.Services.TryAddEnumerable(
            ServiceDescriptor.Singleton<ILoggerProvider, XunitLoggerProvider>());

        LoggerProviderOptions.RegisterProviderOptions
            <XunitLoggerConfiguration, XunitLoggerProvider>(builder.Services);

        return builder;
    }

    public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper testOutputHelper, Action<XunitLoggerConfiguration> configure)
    {
        builder.AddXunit(testOutputHelper);
        builder.Services.Configure(configure);

        return builder;
    }
}

이전 ConsoleLogger 예제와 동일한 방식으로 사용됩니다.
[Fact]
public async Task Test_Custom_XunitLoggingBuilder()
{
    var configuration = new ConfigurationBuilder().Build();
    var serviceProvider = new ServiceCollection()
        .AddLogging(loggingBuilder => {
            loggingBuilder.AddXunit(_testOutputHelper);
        })
        .AddEcho(configuration)
        .BuildServiceProvider();
    var sut = serviceProvider.GetRequiredService<IEchoService>();
    var testInput = "Scenario: custom logging builder";
    var testResult = await sut.Echo(testInput).ConfigureAwait(false);
    testResult.Should().Be(testInput, "the input should have been returned");
}


내가 처음 이 테스트를 했을 때 나는 매우 곤혹스러웠다.나는 우리가 이전에 한 Console Logger 테스트의 컨트롤러 출력만 볼 수 있다.빠른 구글 검색으로 찾아냈다solution.dotnet 테스트 실행 프로그램 사용 XunitLoggingProvider 을 알려 주어야 합니다.팀 전체에게 더 이상 간단하게 달리기dotnet test --logger:"console;verbosity=detailed"는 진정한 해결 방안이 아니라고 알려준다.다행히도 우리는 dotnet test로 일을 간소화할 수 있다.
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
    <LoggerRunSettings>
        <Loggers>
            <Logger friendlyName="console" enabled="True">
                <Configuration>
                    <Verbosity>detailed</Verbosity>
                </Configuration>
            </Logger>
        </Loggers>
    </LoggerRunSettings>
</RunSettings>

그러나 매번 현식 전달dotnet test --settings runsettings.xml마다 어떤 문제도 해결할 수 없다.Microsoft Docs에서 해결 방안을 찾았습니다.MSBuild 사용--settings을 알려 드리면 문제를 해결할 수 있습니다.만약 우리가 지금 운행하고 있다면, 우리는 정확한 출력을 얻을 것이다.예를 들어 프로젝트의 루트 디렉터리에 RunSettingsFilePath를 추가할 수 있습니다.
<Project>
  <PropertyGroup>
    <RunSettingsFilePath>$(MSBuildThisFileDirectory)runsettings.xml</RunSettingsFilePath>
  </PropertyGroup>
</Project>

결어


나는 내가 처음으로 이 화제를 쓴 사람이 아니라는 것을 알고 있지만, 나는 이 주제에 대해 새로운 견해를 제공할 수 있기를 바란다.서로 다른 기술은 모두 각자의 장점을 가지고 있다.나는 이미 다른 장소에서 이 세 가지 옵션을 사용했고, 많은 경우에, Null Logger는 실행 가능한 옵션이라는 것을 알려 줍니다.10번 중 9번은 테스트할 업무 논리에만 관심을 가질 수 있습니다.마지막 남은 시간 동안 나는 모두가 알고 있는 프로그래밍 지혜: 상황을 보고 결정할 수 밖에 없다.
예전과 같이, 만약 당신에게 무슨 문제가 있으면 언제든지 연락하세요.당신은 어떤 건의나 선택이 있습니까?나는 그들의 이야기를 매우 듣고 싶다.
본고의 상응하는 소스 코드는 GitHub에 있다.
다음에 봐요. 다들 건강하고 즐겁게 지내세요.🧸!

좋은 웹페이지 즐겨찾기