MELT 1.1.0
MELT
Testing Library for Microsoft Extensions Logging.
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.
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".
This project supports all currently supported versions of .NET and ASP.NET Core, including full framework. Please refer to the documentation for examples and compatibility details.
If you are upgrading from version 0.4 or below, you can optionally migrate to the new syntax. See Upgrade from 0.4 and below for more info.
If you are upgrading from version 0.6, you can optionally migrate to the new accessors for getting all scopes. See Upgrade from 0.6 for more info.
Index
- Quickstart
- Assertions
- Quickstart for ASP.NET Core integration tests
- Compatibility
- Serilog compatibility using Serilog.Extensions.Logging
- Serilog compatibility using Serilog.AspNetCore
- NLog compatibility using NLog.Web.AspNetCore
- Upgrade from 0.6
- Upgrade from 0.4 and below
Quickstart
Install the NuGet package MELT
<PackageReference Include="MELT" Version="1.1.0" />Note: due to a breaking change in
Microsoft.Extensions.Logging3.1, if you are testing a project that references onlyMicrosoft.Extensions.Logging.Abstractions3.1+, you need to add a reference toMicrosoft.Extensions.Logging3.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.v3
Install the NuGet package MELT.Xunit.v3
<PackageReference Include="MELT.Xunit.v3" Version="1.1.0" />Use the
LoggingAssert.Contains(...)helpers. For example, to test that a single log has been emitted and it had a propertynumberwith value42:var log = Assert.Single(loggerFactory.Sink.LogEntries); LoggingAssert.Contains("number", 42, log.Properties);
Easily test log or scope properties with xUnit v2
Install the NuGet package MELT.Xunit
<PackageReference Include="MELT.Xunit" Version="1.1.0" />Use the
LoggingAssert.Contains(...)helpers. For example, to test that a single log has been emitted and it had a propertynumberwith value42: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="1.1.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 usingILogger<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
WithWebHostBuilderon 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>()orFilterByLoggerName(string name).Or, if you prefer, you can use the
AddTest(...)extension method in theConfigureLogging(...)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
ConfigureWebHostimplementation in your customWebApplicationFactory<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 yourxUnittests. 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 compatible 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 (now published as 2.3) available for full .NET Framework
- 8.0
- 9.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.csof your ASP.NET Core applications, defining aLoggerProviderCollectionto be able to hook into the logging from the tests later on. Then, pass it to theUseSerilog()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="1.1.0" />Define a Serilog logger, setting it up to write to the providers' collection we had previously added to
Program.csLog.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 usingILogger<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
WithWebHostBuilderon 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>()orFilterByLoggerName(string name).Or, if you prefer, you can use the
AddSerilogTest(...)extension method in theConfigureLogging(...)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
ConfigureWebHostimplementation in your customWebApplicationFactory<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 yourxUnittests. 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.v3
Install the NuGet package MELT.Xunit.v3
<PackageReference Include="MELT.Xunit.v3" Version="1.1.0" />Use the
LoggingAssert.Contains(...)helpers. For example, to test that a single log has been emitted and it had a propertynumberwith value42: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);
Easily test log or scope properties with xUnit v2
Install the NuGet package MELT.Xunit
<PackageReference Include="MELT.Xunit" Version="1.1.0" />Use the
LoggingAssert.Contains(...)helpers. For example, to test that a single log has been emitted and it had a propertynumberwith value42: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.Logging3.1, if you are testing a project that references onlyMicrosoft.Extensions.Logging.Abstractions3.1+, you need to add a reference toMicrosoft.Extensions.Logging3.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);
No packages depend on MELT.
.NET Standard 2.0
- Microsoft.Extensions.DependencyInjection (>= 2.1.0)
- Microsoft.Extensions.Logging (>= 2.1.0)
| Version | Downloads | Last updated |
|---|---|---|
| 1.1.0 | 3 | 10/06/2025 |
| 1.1.0-preview.1 | 3 | 10/07/2025 |
| 1.0.0 | 4 | 06/15/2025 |
| 1.0.0-rc.2 | 4 | 06/15/2025 |
| 1.0.0-rc.1 | 4 | 06/15/2025 |
| 0.9.0 | 11 | 01/03/2025 |
| 0.8.0 | 11 | 01/04/2024 |
| 0.7.0 | 9 | 01/03/2025 |
| 0.6.0 | 10 | 01/03/2025 |
| 0.6.0-preview.1 | 10 | 01/03/2025 |
| 0.5.3 | 10 | 01/03/2025 |
| 0.5.2 | 10 | 01/03/2025 |
| 0.5.1 | 10 | 01/03/2025 |
| 0.5.0 | 9 | 01/03/2025 |
| 0.4.0 | 9 | 01/03/2025 |
| 0.3.0 | 10 | 01/03/2025 |
| 0.2.0 | 10 | 01/03/2025 |
| 0.1.0 | 10 | 01/03/2025 |