Skip to content
/ MELT Public

MELT is a free, open source, testing library for the .NET Standard Microsoft Extensions Logging library. It is a solution to easily test logs.

License

Notifications You must be signed in to change notification settings

alefranz/MELT

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

37 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MELT

Testing Library for Microsoft Extensions Logging.

Build Status MELT Nuget MELT.AspNetCore Nuget MELT.Serilog Nuget MELT.Serilog.AspNetCore Nuget MELT.Xunit Nuget

About MELT

MELT is a free, open-source, testing library for the .NET Standard Microsoft Extensions Logging library. It is a solution to easily test logs.

It is a repackaging with a sweetened API and some omissions of Microsoft.Extensions.Logging.Testing, a library used internally in ASP.NET Core for testing the logging, given that there is currently no plan to offer an official package for it.

It is licensed under Apache License 2.0. Most of the code is copyrighted by the .NET Foundation as mentioned in the files headers.

If you like this project please don't forget to star it on GitHub or let me know with a tweet.

You can find an explanation on the advantages of using this library and the importance of testing logs on the blog post "How to test logging when using Microsoft.Extensions.Logging".

If you are upgrading to version 0.5, you can optionally migrate to the new syntax. See Upgrade from 0.4 and below for more info.

Index

Quickstart

  • Install the NuGet package MELT

    <PackageReference Include="MELT" Version="0.9.0" />

    Note: due to a breaking change in Microsoft.Extensions.Logging 3.1, if you are testing a project that references only Microsoft.Extensions.Logging.Abstractions 3.1+, you need to add a reference to Microsoft.Extensions.Logging 3.1+ in your test project:

    <PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.0" />
  • Get a test logger factory

    var loggerFactory = TestLoggerFactory.Create();
  • Get a logger from the factory, as usual, to pass to your fixture.

    var logger = loggerFactory.CreateLogger<Sample>();

Assertions

Assert log entries

The logger factory exposes a property Sink to access the sink that collects the logs. The sink exposes a property LogEntries that enumerates all the captured logs. Each entry exposes all the relevant property for a log.

For example, to test with xUnit that a single log has been emitted and it has a specific message:

var log = Assert.Single(loggerFactory.Sink.LogEntries);
Assert.Equal("The answer is 42", log.Message);

Assert scopes

Each log entry exposes a property Scopes to have the scopes active for the particular logger captured with that entry. Note that the scopes of a log entry are local to a specific logger and async context.

var log = Assert.Single(loggerFactory.Sink.LogEntries);
var log = Assert.Single(log.Scopes);
Assert.Equal("This scope's answer is 42", scope.Message);

It is also possible to get all the scopes generated across all loggers. The logger factory exposes a property Sink to access the sink that collects the logs. The sink exposes a property Scopes that enumerates all the captured scopes.

For example, to test with xUnit that a single scope has been emitted and it had a specific message:

var scope = Assert.Single(loggerFactory.Sink.Scopes);
Assert.Equal("I'm in the GET scope", scope.Message);

Assert log original format

The original format used to generate the log entry, before the message is rendered, is captured in the property OriginalMessage.

var log = Assert.Single(loggerFactory.Sink.LogEntries);
Assert.Equal("The answer is {number}", log.OriginalFormat);

Assert exceptions in log entries

The log entry exposes a property Exception which contains the exception captured by the logger.

For example, to test with xUnit that a single log has been emitted with a specific exception and assert a exception property:

var log = Assert.Single(loggerFactory.Sink.LogEntries);
var exception = Assert.IsType<ArgumentNullException>(log.Exception);
Assert.Equal("foo", exception.ParamName);

Easily test log or scope properties with xUnit

  • Install the NuGet package MELT.Xunit

    <PackageReference Include="MELT.Xunit" Version="0.9.0" />
  • Use the LoggingAssert.Contains(...) helpers. For example, to test that a single log has been emitted and it had a property number with value 42:

    var log = Assert.Single(loggerFactory.Sink.LogEntries);
    LoggingAssert.Contains("number", 42, log.Properties);

And much more

You can assert againt all the characteristic of a log entry: EventId, Exception, LoggerName, LogLevel, Message, OriginalFormat, Properties and Scope.

Full example

See Samples

Quickstart for ASP.NET Core integration tests

  • Install the NuGet package MELT.AspNetCore

    <PackageReference Include="MELT.AspNetCore" Version="0.9.0" />
  • Use the UseTestLogging(...) extension method to add a test logger to the test web host builder, where you can also customize the behaviour.

    For example to filter all log entries and scopes not generated by loggers consumed in the SampleWebApplication.* namespace (this filters the logger name so it assumes you are using ILogger<T> or following the default naming convention for your loggers.)

    This can be done where you are already configuring the web host builder. Configure the logger using WithWebHostBuilder on the factory.

    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Mvc.Testing;
    // ...
    _factory = factory.WithWebHostBuilder(builder => builder
        .UseTestLogging(options => options.FilterByNamespace(nameof(SampleWebApplication))));

    You can also filter by logger name using FilterByTypeName<T>() or FilterByLoggerName(string name).

    Or, if you prefer, you can use the AddTest(...) extension method in the ConfigureLogging(...) section.

    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Mvc.Testing;
    using Microsoft.Extensions.Logging;
    // ...
    _factory = factory.WithWebHostBuilder(builder => builder
        .ConfigureLogging(logging => logging.AddTest(options => options.FilterByNamespace(nameof(SampleWebApplication)))));
  • Alternatively, you can configure the logger builder in the ConfigureWebHost implementation in your custom WebApplicationFactory<TStartup>.

    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Mvc.Testing;
    
    namespace SampleWebApplication.IntegrationTests
    {
        public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup>
            where TStartup : class
        {
            protected override void ConfigureWebHost(IWebHostBuilder builder)
            {
                builder.UseTestLogging(options => options.FilterByNamespace(nameof(SampleWebApplication)));
            }
        }
    }

    You can then retrieve the sink to assert against using the extension method GetTestLoggerSink() on the factory.

    Please note that in this case, all tests sharing the same factory will get the same sink. You can reset it between tests with Clear() in the constructor of your xUnit tests. For example:

    public class LoggingTestWithInjectedFactory : IClassFixture<CustomWebApplicationFactory<Startup>>
    {
        private readonly CustomWebApplicationFactory<Startup> _factory;
    
        public LoggingTestWithInjectedFactory(CustomWebApplicationFactory<Startup> factory)
        {
            _factory = factory;
            // In this case, the factory will be reused for all tests, so the sink will be shared as well.
            // We can clear the sink before each test execution, as xUnit will not run this tests in parallel.
            _factory.GetTestLoggerSink().Clear();
            // When running on 2.x, the server is not initialized until it is explicitly started or the first client is created.
            // So we need to use:
            // if (_factory.TryGetTestLoggerSink(out var testLoggerSink)) testLoggerSink.Clear();
        }
    }

    The logger will be automatically injected with Dependency Injection.

Assert log entries and scopes

Once you access the sink with _factory.GetTestLoggerSink() you get access to the property LogEntries that enumerates all the captured logs and Scopes which enumerated all the captured Scopes. You will then be able to do all the assertions like described above in Assertions

For example, to test with xUnit that a single log has been emitted and it had a specific message:

var log = Assert.Single(_factory.GetTestLoggerSink().LogEntries);
Assert.Equal("The answer is 42", log.Message);

Full example

See LoggingTest or LoggingTestWithInjectedFactory.

Compatibility

This library is compatibe with Microsoft.Extensions.Logging 2.0+. When used for integration tests of ASP.NET Core applications, it supports all the currently supported versions of ASP.NET Core: 2.1 LTS, 3.1 LTS and 5.0, but also the now deprecated 2.2 and 3.0.

Serilog compatibility using Serilog.Extensions.Logging

If you are using Serilog.Extensions.Logging the integration is straightforward as this library is fully compliant with Microsoft.Extensions.Logging.

Simply follow the main instruction as the fact that you are plugging Serilog as the provider does not alter the behaviour.

Full example

See LoggingTest or LoggingTestWithInjectedFactory.

Serilog compatibility using Serilog.AspNetCore

Unfortunately, Serilog.AspNetCore doesn't plug nicely into Microsoft.Extensions.Logging as it replaces the logger factory and brings in an opinionated behaviour.

However, MELT has specific support to allow to write tests against the Serilog produced logs, also allowing you to verify the Serilog behaviours (e.g. object expansion).

  • Modify your Program.cs of your ASP.NET Core applications, defining a LoggerProviderCollection to be able to hook into the logging from the tests later on. Then, pass it to the UseSerilog() extension method of the web host builder.

    public class Program
    {
        public static readonly LoggerProviderCollection Providers = new LoggerProviderCollection();  // <---
    
        public static int Main(string[] args)
        {
            // ...
        }
    
        public static IHostBuilder CreateHostBuilder(string[] args)
        {
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder
                        // ...
                        .UseSerilog(providers: Providers);  // <---
                });
        }
    }
  • Now go back to your integration tests project, and install the NuGet package MELT.Serilog.AspNetCore

    <PackageReference Include="MELT.Serilog.AspNetCore" Version="0.9.0" />
  • Define a Serilog logger, setting it up to write to the providers' collection we had previously added to Program.cs

    Log.Logger = new LoggerConfiguration()
        .MinimumLevel.Verbose()
        .Enrich.FromLogContext()
        .WriteTo.Providers(Program.Providers)  // <---
        .CreateLogger();
  • Use the UseSerilogTestLogging(...) extension method to add a test logger to the test web host builder, where you can also customize the behaviour.

    For example to filter all log entries and scopes not generated by loggers consumed in the SampleWebApplication.* namespace (this filters the logger name so it assumes you are using ILogger<T> or following the default naming convention for your loggers.)

    This can be done where you are already configuring the web host builder. Configure the logger using WithWebHostBuilder on the factory.

    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Mvc.Testing;
    // ...
    _factory = factory.WithWebHostBuilder(builder => builder
        .UseSerilogTestLogging(options => options.FilterByNamespace(nameof(SampleWebApplicationSerilogAlternate))));

    You can also filter by logger name using FilterByTypeName<T>() or FilterByLoggerName(string name).

    Or, if you prefer, you can use the AddSerilogTest(...) extension method in the ConfigureLogging(...) section.

    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Mvc.Testing;
    using Microsoft.Extensions.Logging;
    // ...
    _factory = factory.WithWebHostBuilder(builder => builder
        .ConfigureLogging(logging => logging.AddSerilogTest(options => options.FilterByNamespace(nameof(SampleWebApplicationSerilogAlternate)))));
  • Alternatively, you can configure the logger builder in the ConfigureWebHost implementation in your custom WebApplicationFactory<TStartup>.

    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Mvc.Testing;
    using Serilog;
    
    namespace SampleWebApplicationSerilogAlternate.IntegrationTests
    {
        public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup>
            where TStartup : class
        {
            public CustomWebApplicationFactory()
            {
                Log.Logger = new LoggerConfiguration()
                    .MinimumLevel.Verbose()
                    .Enrich.FromLogContext()
                    .WriteTo.Providers(Program.Providers)
                    .CreateLogger();
            }
    
            protected override void ConfigureWebHost(IWebHostBuilder builder)
            {
                builder.UseSerilogTestLogging(options =>
                {
                    options.FilterByNamespace(nameof(SampleWebApplicationSerilogAlternate));
                });
            }
    
            protected override void Dispose(bool disposing)
            {
                if (true)
                {
                    Log.CloseAndFlush();
                }
            }
        }
    }

    You can then retrieve the sink to assert against using the extension method GetSerilogTestLoggerSink() on the factory.

    Please note that in this case, all tests sharing the same factory will get the same sink. You can reset it between tests with Clear() in the constructor of your xUnit tests. For example:

    public class LoggingTestWithInjectedFactory : IClassFixture<CustomWebApplicationFactory<Startup>>
    {
        private readonly CustomWebApplicationFactory<Startup> _factory;
    
        public LoggingTestWithInjectedFactory(CustomWebApplicationFactory<Startup> factory)
        {
            _factory = factory;
            // In this case, the factory will be reused for all tests, so the sink will be shared as well.
            // We can clear the sink before each test execution, as xUnit will not run this tests in parallel.
            _factory.GetSerilogTestLoggerSink().Clear();
            // When running on 2.x, the server is not initialized until it is explicitly started or the first client is created.
            // So we need to use:
            // if (_factory.TryGetSerilogTestLoggerSink(out var testLoggerSink)) testLoggerSink.Clear();
        }
    }

    The logger will be automatically injected with Dependency Injection.

Assert log entries

The sink exposes a property LogEntries that enumerates all the logs captured. Each entry exposes all the relevant property for a log.

For example, to test with xUnit that a single log has been emitted and it had a specific message:

var log = Assert.Single(_factory.GetSerilogTestLoggerSink().LogEntries);
Assert.Equal("Hello \"World\"!", log.Message);

Please note that Serilog adds double quotes around parameters.

Assert scopes on an entry

The log entry exposes a property Scopes that enumerates all the scopes captured for that log entry.

For example, to test with xUnit that a single scope has been emitted and it had a specific message:

var log = Assert.Single(_factory.GetSerilogTestLoggerSink().LogEntries);
var scope = Assert.Single(log.Scope);
Assert.Equal(new ScalarValue("I'm in the GET scope"), scope);

The scope is preserved in the Serilog format, so you can use the Serilog DictionaryValue, ScalarValue, SequenceValue or StructureValue.

If you have multiple nested scopes, you can assert with:

Assert.Collection(log.Scope,
    x => Assert.Equal(new ScalarValue("A top level scope"), x),
    x => Assert.Equal(new ScalarValue("I'm in the GET scope"), x)
);

Assert message format

var log = Assert.Single(loggerFactory.LogEntries);
Assert.Equal("The answer is {number}", log.OriginalFormat);

Easily test log or scope properties with xUnit

  • Install the NuGet package MELT.Xunit

    <PackageReference Include="MELT.Xunit" Version="0.9.0" />
  • Use the LoggingAssert.Contains(...) helpers. For example, to test that a single log has been emitted and it had a property number with value 42:

    var log = Assert.Single(_factory.GetSerilogTestLoggerSink().LogEntries);
    LoggingAssert.Contains("place", "World", log.Properties);

    Note that if you have added to the scope a dictionary, Serilog will only add the properties to the log entry it self, without create a scope:

    Assert.Empty(log.Scope);
    LoggingAssert.Contains("foo", "bar", log.Properties);
    LoggingAssert.Contains("answer", 42, log.Properties);

And much more

You can assert againt all the characteristic of a log entry: EventId, Exception, LoggerName, LogLevel, Message, Properties, OriginalFormat and Scope.

Full example

See LoggingTest or LoggingTestWithInjectedFactory.

NLog compatibility using NLog.Web.AspNetCore

If you are using NLog.Web.AspNetCore the integration is straightforward as this library is fully compliant with Microsoft.Extensions.Logging.

Simply follow the main instruction as the fact that you are plugging NLog as the provider does not alter the behaviour.

Full example

See LoggingTest or LoggingTestWithInjectedFactory.

Upgrade from 0.6

If you follow the deprecation warnings on LogEntry.Scope, you will be able to easily migrate to the new LogEntry.Scopes, which contains all the scopes active for the specific log entry, which means the scopes that were active for the particular logger and async context when the log entry had been generated.

Also notes that the LogEntry.Scope now returns the inner scope for the speicifc logger and async context, instead of the previously unexpected behaviour of returning the latest scope created on the specific logger, even if not active for that entry.

Asserting scope.

Assert.Equal("I'm in the GET scope", log.Scope.Message);
// become
var scope = Assert.Single(log.Scopes);
Assert.Equal("I'm in the GET scope", scope.Message);

Upgrade from 0.4 and below

The library is still backward compatible, however, if you follow the deprecation warnings, you will be able to easily migrate to the new simplified syntax.

Note: due to a breaking change in Microsoft.Extensions.Logging 3.1, if you are testing a project that references only Microsoft.Extensions.Logging.Abstractions 3.1+, you need to add a reference to Microsoft.Extensions.Logging 3.1+ in your test project:

<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.0" />

Here are some common examples:

Setting up logger factory for tests.

var loggerFactory = MELTBuilder.CreateLoggerFactory();
// become
var loggerFactory = TestLoggerFactory.Create();

Setting up logger factory for tests, filtering messages by component.

var loggerFactory = MELTBuilder.CreateLoggerFactory(options => options.FilterByTypeName<MyClass>());
// become
var loggerFactory = TestLoggerFactory.Create(options => options.FilterByTypeName<MyClass>());

Accessing captured logs from the factory

var log = Assert.Single(loggerFactory.LogEntries);
// become
var log = Assert.Single(loggerFactory.Sink.LogEntries);

Accessing the original format of a log entry

Assert.Equal("More is less.", log.Format);
// become
Assert.Equal("More is less.", log.OriginalFormat);

Assert against log properties

LogValuesAssert.Contains("number", 42, log);
// become
LoggingAssert.Contains("number", 42, log.Properties);

Assert against scope properties

LogValuesAssert.Contains("number", 42, scope);
// become
LoggingAssert.Contains("number", 42, scope.Properties);

And to assert log properties, the using is no longer needed

using MELT.Xunit;
// no longer needed :)

Upgrade of ASP.NET Core Integration Tests

Setting up the web application factory with the test logger

_sink = MELTBuilder.CreateTestSink(options => options.FilterByNamespace(nameof(SampleWebApplication)));
_factory = factory.WithWebHostBuilder(builder => builder.ConfigureLogging(logging => logging.AddTestLogger(_sink)));
// become
_factory = factory.WithWebHostBuilder(builder => builder.UseTestLogging(options => options.FilterByNamespace(nameof(SampleWebApplication))));

Accessing the captured logs

var log = Assert.Single(_sink.LogEntries);
// become
var log = Assert.Single(_factory.GetTestLoggerSink().LogEntries);

About

MELT is a free, open source, testing library for the .NET Standard Microsoft Extensions Logging library. It is a solution to easily test logs.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published