Implement full logging in Integration Test
Asked Answered
S

3

5

I'm creating a new app in .Net Core 3.1.

I have the database created, and now I'm working on the business logic, which is in a collection of services. Before I start to create the API or the UI (ie: any web-app type project), I want to 100% get all of the Data Access and Services working as expected first... including logging. To make sure this is all working together as it should, I want to create some integration tests.

The problem I am running into is I am REALLY struggling with how to get logging working. Every tutorial, document, and all of the examples I can find assume you have a Program.cs and Startup.cs.

NOTE 1: I want the logging to work just as it would in production, which means all the way to SQL. No mocking. No substitution. No replacing.

NOTE 2: I don't care so much about logging from the test. I want logging to work in the service.

Here is an example of an integration test (xUnit) class that I have so far. It's working but has no logging.

namespace MyApp.Test
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IAccountService, AccountService>();
            services.AddTransient<IAppSettings, AppSettings>();
        }
    }

    public class AccountServiceTests
    {
        private readonly IAccountService _account;
        private readonly IAppSettings _settings;

        public AccountServiceTests(IAccountService account, IAppSettings settings)
        {
            _account = account;
            _settings = settings;
        }

        [Fact]
        public async Task AccountService_CreateAccount()
        {
            Account account = new Account( {...} );
            bool result = _account.CreateAccount(account);
            Assert.True(result);
        }
    }
}

The DI is working because of NuGet Xunit.DependencyInjection.

And then in the service...

public class AccountService : ServiceBase, IAccountService
{
        protected readonly ILogger _logger;
        protected readonly IAppSettings _settings;

        public AccountService(ILogger<AccountService> logger, IAppSettings settings) 
        {
            _logger = logger;
            _settings = settings;
        }

        public bool CreateAccount()
        {
            // do stuff
            _logger.Log(LogLevel.Information, "An account was created."); // I WANT THIS TO END UP IN SQL, EVEN FROM THE TEST.
        }
}

The test passes, and the account is properly created in the database. However, as best as I can tell, this line doesn't do anything:

_logger.Log(LogLevel.Information, "An account was created.");

I understand why. Microsoft.Extensions.Logging is just an abstraction, and I need to implement some concrete logging (with SeriLog or Log4Net, etc.)

This brings me back to my original question: For the life of me, I can not find a working tutorial on how to get either one of those (SeriLog or Log4Net) working within an integration test (xUnit in particular).

Any help, or point in the right direction, or a link to a tutorial would be wonderful. Thanks.

Sexuality answered 12/1, 2021 at 19:47 Comment(3)
Hi again Casey, I think the issue isn't a lack of a concrete logger. Rather, you'll probably need to provide a basic logging configuration when setting up the test fixture. Normally this would be done by calling ConfigureLogging on a host builder, but in this particular case I am unsure. I do not have .Net Core available on my work PC. I suggest looking at setting up logging in a .Net Core console application, maybe.Idellaidelle
@Amy, thanks. I think you are spot on. My total lack of finding anything on this makes me think I'm barking up the wrong tree. I need an expert to consult with.Sexuality
See also AspNetCore.TestHost: How to get log output from the in-process server?Hanforrd
R
5

Add Logging to the service collection using LoggingServiceCollectionExtensions.AddLogging

public class Startup {
    public void ConfigureServices(IServiceCollection services) {
        services.AddLogging(builder => 
            builder.ClearProviders()
                .Add{ProverNameHere}()

            // ... other logging configuration
        );
        services.AddTransient<IAccountService, AccountService>();
        services.AddTransient<IAppSettings, AppSettings>();
    }
}

This will add the factory and open generic for ILogger<> so that they can be injected where needed.

Configure the logging as desired for where that information should go.

There are built-in providers that ASP.NET Core includes as part of the shared framework, but since this is an isolated test you have to add the desired providers as needed.

For example

//...

services.AddLogging(builder => builder.AddConsole().AddDebug());

//...

Console

The Console provider logs output to the console.

Debug

The Debug provider writes log output by using the System.Diagnostics.Debug class. Calls to System.Diagnostics.Debug.WriteLine write to the Debug provider.

Logging output from dotnet run and Visual Studio

Logs created with the default logging providers are displayed:

  • In Visual Studio
    • In the Debug output window when debugging.
    • In the ASP.NET Core Web Server window.
  • In the console window when the app is run with dotnet run.

Logs that begin with "Microsoft" categories are from ASP.NET Core framework code. ASP.NET Core and application code use the same logging API and providers.

Reference: Logging in .NET Core and ASP.NET Core

Rosaliarosalie answered 12/1, 2021 at 21:4 Comment(1)
Got it! I am finally able to see the logs using this: services.AddLogging(builder => builder.ClearProviders().AddDebug());. Without the ClearProviders() for some reason it was not writing to the output window. Now it is. Thank you!Sexuality
D
2

If you just want to see that the logging is working in xUnit, add something like the following code to the constructor of your xUnit test, and pass it on to the base constructor as follows. (I'm assuming here you're using a WebApplicationFactory to start up the service in the test.)

protected readonly WebApplicationFactory<Startup> Factory;

public AccountServiceTests(IAccountService account, IAppSettings settings, ITestOutputHelper testOutputHelper) : base( testOutputHelper )
{
    _account = account;
    _settings = settings;

    Factory = new WebApplicationFactory<Startup>().WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddLogging(logBuilder => logBuilder.AddXUnit(testOutputHelper));
            });
        });
}

This will cause your log messages to be shown in the output of the test.

Note that the details here might vary somewhat for what you have, but this is the basic idea: xUnit passes in the testOutputHelper and you use it to initialize the logging system for the test.

Programatically verifying that the proper log messages were written by your test is another matter. I don't have a ready answer for that.

Derogative answered 12/1, 2021 at 20:14 Comment(4)
Thanks Mark, but I don't want to test logging from the test. I want to test it from within the service.Sexuality
@CaseyCrookston But you want to test logging from within the service while running the service under xUnit, right? If so, this is what you want. I'm modifying my answer to include another initialization step I forgot.Derogative
But you want to test logging from within the service while running the service under xUnit, right? Yes! 100% right!Sexuality
Programatically verifying that the proper log messages were written by your test - try #66532916Hanforrd
P
1

Configure a logger factory

using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddFilter("Microsoft", LogLevel.Warning)
        .AddFilter("System", LogLevel.Warning)
        .AddFilter("LoggingConsoleApp.Program", LogLevel.Debug)
        .AddConsole()
        .AddEventLog();
});

and then register it as a service

services.AddSingleton(loggerFactory);
Puppis answered 12/1, 2021 at 20:5 Comment(6)
Martin, thank you! Based on the sample test class in my OP, where would the first block of code go?Sexuality
I am not very familiar with Xunit.DependencyInjection nugget, but it could probably go into ConfigureServices method.Puppis
System.AggregateException : One or more errors occurred. (Cannot access a disposed object. Object name: 'LoggerFactory'.) (The following constructor parameters did not have matching fixture data: IAccountService account, IAppSettings settings)Sexuality
@CaseyCrookston The using is disposing of the factory. Just remove that bit.Idellaidelle
Ok the test runs now with no error, calling the service and creating the account. So now the question is, when I call _logger.LogInformation() in the service, where do I see the output?Sexuality
You can add file logging, example is here.Puppis

© 2022 - 2024 — McMap. All rights reserved.