Skip to content

Instantly share code, notes, and snippets.

@danielcrenna
Created March 5, 2021 18:18
Show Gist options
  • Save danielcrenna/68285f4187d0f7e5eff325f5014ee0e1 to your computer and use it in GitHub Desktop.
Save danielcrenna/68285f4187d0f7e5eff325f5014ee0e1 to your computer and use it in GitHub Desktop.
ASP.NET Core: Show web application logs in in-memory integration tests
// Copyright (c) Daniel Crenna. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at http://mozilla.org/MPL/2.0/.
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
using Xunit.Abstractions;
namespace BetterTesting.Tests
{
/// <summary>
/// <see href="https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-5.0" />
/// </summary>
public class EndpointTests : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _factory;
public EndpointTests(ITestOutputHelper output, WebApplicationFactory<Startup> factory)
{
_factory = factory.WithTestLogging(output);
}
[Theory]
[InlineData("/WeatherForecast")]
public async Task Get_Route_Returns_Response(string url)
{
var client = _factory.CreateClient(new WebApplicationFactoryClientOptions {AllowAutoRedirect = false});
var response = await client.GetAsync(url);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}
}
D:\>dotnet test -l "console;verbosity=detailed"
Passed BetterTesting.Tests.EndpointTests.Get_Route_Returns_Response(url: "/WeatherForecast") [225 ms]
Standard Output Messages:
warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[FailedToDeterminePort]
=> SpanId:57b5606a144e4c47, TraceId:5e8d203c95e177488e0ebf47fe2c4cbe, ParentId:0000000000000000=> RequestPath:/WeatherForecast RequestId:0HM700ALOQ7QQ
Failed to determine the https port for redirect.
dbug: BetterTesting.Controllers.WeatherForecastController[0]
=> SpanId:57b5606a144e4c47, TraceId:5e8d203c95e177488e0ebf47fe2c4cbe, ParentId:0000000000000000=> RequestPath:/WeatherForecast RequestId:0HM700ALOQ7QQ=> BetterTesting.Controllers.WeatherForecastController.Get (BetterTesting)
Creating fake weather forecasts...
// Copyright (c) Daniel Crenna. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at http://mozilla.org/MPL/2.0/.
using System;
using System.Text;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using Xunit.Abstractions;
namespace BetterTesting.Tests
{
internal static class WebApplicationFactoryExtensions
{
#region Test Logging
/// <summary>
/// Redirect all application logging to the test console.
/// </summary>
/// <typeparam name="TStartup">The web application under test</typeparam>
/// <param name="factory">The chained factory to enable test logging on</param>
/// <param name="output">The current instance of the Xunit test output helper</param>
/// <returns></returns>
public static WebApplicationFactory<TStartup> WithTestLogging<TStartup>(this WebApplicationFactory<TStartup> factory, ITestOutputHelper output) where TStartup : class
{
return factory.WithWebHostBuilder(builder =>
{
builder.ConfigureLogging(logging =>
{
// check if scopes are used in normal operation
var useScopes = logging.UsesScopes();
// remove other logging providers, such as remote loggers or unnecessary event logs
logging.ClearProviders();
logging.Services.AddSingleton<ILoggerProvider>(r => new XunitLoggerProvider(output, useScopes));
});
});
}
private sealed class XunitLogger : ILogger
{
private const string ScopeDelimiter = "=> ";
private const string Spacer = " ";
private const string Trace = "trce";
private const string Debug = "dbug";
private const string Info = "info";
private const string Warn = "warn";
private const string Error = "fail";
private const string Critical = "crit";
private readonly string _categoryName;
private readonly bool _useScopes;
private readonly ITestOutputHelper _output;
private readonly IExternalScopeProvider _scopes;
public XunitLogger(ITestOutputHelper output, IExternalScopeProvider scopes, string categoryName, bool useScopes)
{
_output = output;
_scopes = scopes;
_categoryName = categoryName;
_useScopes = useScopes;
}
public bool IsEnabled(LogLevel logLevel)
{
return logLevel != LogLevel.None;
}
public IDisposable BeginScope<TState>(TState state)
{
return _scopes.Push(state);
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
Func<TState, Exception, string> formatter)
{
var sb = new StringBuilder();
switch (logLevel)
{
case LogLevel.Trace:
sb.Append(Trace);
break;
case LogLevel.Debug:
sb.Append(Debug);
break;
case LogLevel.Information:
sb.Append(Info);
break;
case LogLevel.Warning:
sb.Append(Warn);
break;
case LogLevel.Error:
sb.Append(Error);
break;
case LogLevel.Critical:
sb.Append(Critical);
break;
case LogLevel.None:
break;
default:
throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null);
}
sb.Append(": ").Append(_categoryName).Append('[').Append(eventId).Append(']').AppendLine();
if (_useScopes && TryAppendScopes(sb))
sb.AppendLine();
sb.Append(Spacer);
sb.Append(formatter(state, exception));
if (exception != null)
{
sb.AppendLine();
sb.Append(Spacer);
sb.Append(exception);
}
var message = sb.ToString();
_output.WriteLine(message);
}
private bool TryAppendScopes(StringBuilder sb)
{
var scopes = false;
_scopes.ForEachScope((callback, state) =>
{
if (!scopes)
{
state.Append(Spacer);
scopes = true;
}
state.Append(ScopeDelimiter);
state.Append(callback);
}, sb);
return scopes;
}
}
private sealed class XunitLoggerProvider : ILoggerProvider, ISupportExternalScope
{
private readonly ITestOutputHelper _output;
private readonly bool _useScopes;
private IExternalScopeProvider _scopes;
public XunitLoggerProvider(ITestOutputHelper output, bool useScopes)
{
_output = output;
_useScopes = useScopes;
}
public ILogger CreateLogger(string categoryName)
{
return new XunitLogger(_output, _scopes, categoryName, _useScopes);
}
public void Dispose()
{
}
public void SetScopeProvider(IExternalScopeProvider scopes)
{
_scopes = scopes;
}
}
private static bool UsesScopes(this ILoggingBuilder builder)
{
var serviceProvider = builder.Services.BuildServiceProvider();
// look for other host builders on this chain calling ConfigureLogging explicitly
var options = serviceProvider.GetService<SimpleConsoleFormatterOptions>() ??
serviceProvider.GetService<JsonConsoleFormatterOptions>() ??
serviceProvider.GetService<ConsoleFormatterOptions>();
if (options != default)
return options.IncludeScopes;
// look for other configuration sources
// See: https://docs.microsoft.com/en-us/dotnet/core/extensions/logging?tabs=command-line#set-log-level-by-command-line-environment-variables-and-other-configuration
var config = serviceProvider.GetService<IConfigurationRoot>() ?? serviceProvider.GetService<IConfiguration>();
var logging = config?.GetSection("Logging");
if (logging == default)
return false;
var includeScopes = logging?.GetValue("Console:IncludeScopes", false);
if (!includeScopes.Value)
includeScopes = logging?.GetValue("IncludeScopes", false);
return includeScopes.GetValueOrDefault(false);
}
#endregion
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment