Implement full logging in Integration Test - c#

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.

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

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);

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.

Related

ASP.NET Core Integration tests with dotnet-testcontainers

I am trying to add some integration tests for a aspnetcore v6 webapi following the docs - https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-6.0#aspnet-core-integration-tests.
My webapi database is SQLServer. I want the tests to be run against an actual SQLServer db and not in-memory database. I came across dotnet-testcontainers - https://github.com/HofmeisterAn/dotnet-testcontainers and thinking of using this so I do not need to worry about the resetting the db as the container is removed once test is run.
So this is what I plan to do:
Start-up a SQLServer testcontainer before the test web host is started. In this case, the test web host is started using WebApplicationFactory. So the started wen host has a db to connect with. Otherwise the service start will fail.
Run the test. The test would add some test data before its run.
Then remove the SQLServer test container along with the Disposing of test web host.
This way the I can start the test web host that connects to a clean db running in a container, run the tests.
Does this approach sound right? OR Has someone used dotnet-testcontainers to spin up a container for their application tests and what approach worked.
I wrote about this approach here.
You basically need to create a custom WebApplicationFactory and replace the connection string in your database context with the one pointing to your test container.
Here is an example, that only requires slight adjustments to match the MSSQL docker image.
public class IntegrationTestFactory<TProgram, TDbContext> : WebApplicationFactory<TProgram>, IAsyncLifetime
where TProgram : class where TDbContext : DbContext
{
private readonly TestcontainerDatabase _container;
public IntegrationTestFactory()
{
_container = new TestcontainersBuilder<PostgreSqlTestcontainer>()
.WithDatabase(new PostgreSqlTestcontainerConfiguration
{
Database = "test_db",
Username = "postgres",
Password = "postgres",
})
.WithImage("postgres:11")
.WithCleanUp(true)
.Build();
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
services.RemoveProdAppDbContext<TDbContext>();
services.AddDbContext<TDbContext>(options => { options.UseNpgsql(_container.ConnectionString); });
services.EnsureDbCreated<TDbContext>();
});
}
public async Task InitializeAsync() => await _container.StartAsync();
public new async Task DisposeAsync() => await _container.DisposeAsync();
}
And here are the extension methods to replace and initialize your database context.
public static class ServiceCollectionExtensions
{
public static void RemoveDbContext<T>(this IServiceCollection services) where T : DbContext
{
var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<T>));
if (descriptor != null) services.Remove(descriptor);
}
public static void EnsureDbCreated<T>(this IServiceCollection services) where T : DbContext
{
var serviceProvider = services.BuildServiceProvider();
using var scope = serviceProvider.CreateScope();
var scopedServices = scope.ServiceProvider;
var context = scopedServices.GetRequiredService<T>();
context.Database.EnsureCreated();
}
}
There are another two ways to leverage Testcontainers for .NET in-process into your ASP.NET application and even a third way out-of-process without any dependencies to the application.
1. Using .NET's configuration providers
A very simple in-process setup passes the database connection string using the environment variable configuration provider to the application. You do not need to mess around with the WebApplicationFactory. All you need to do is set the configuration before creating the WebApplicationFactory instance in your tests.
The example below passes the HTTPS configuration incl. the database connection string of a Microsoft SQL Server instance spun up by Testcontainers to the application.
Environment.SetEnvironmentVariable("ASPNETCORE_URLS", "https://+");
Environment.SetEnvironmentVariable("ASPNETCORE_Kestrel__Certificates__Default__Path", "certificate.crt");
Environment.SetEnvironmentVariable("ASPNETCORE_Kestrel__Certificates__Default__Password", "password");
Environment.SetEnvironmentVariable("ConnectionStrings__DefaultConnection", _mssqlContainer.ConnectionString);
_webApplicationFactory = new WebApplicationFactory<Program>();
_serviceScope = _webApplicationFactory.Services.GetRequiredService<IServiceScopeFactory>().CreateScope();
_httpClient = _webApplicationFactory.CreateClient();
This example follows the mentioned approach above.
2. Using .NET's hosted service
A more advanced approach spins up the dependent database and seeds it during the application start. It not just helps writing better integration tests, it integrates well into daily development and significantly improves the development experience and productivity.
Spin up the dependent container by implementing IHostedService:
public sealed class DatabaseContainer : IHostedService
{
private readonly TestcontainerDatabase _container = new TestcontainersBuilder<MsSqlTestcontainer>()
.WithDatabase(new DatabaseContainerConfiguration())
.Build();
public Task StartAsync(CancellationToken cancellationToken)
{
return _container.StartAsync(cancellationToken);
}
public Task StopAsync(CancellationToken cancellationToken)
{
return _container.StopAsync(cancellationToken);
}
public string GetConnectionString()
{
return _container.ConnectionString;
}
}
Add the hosted service to your application builder configuration:
builder.Services.AddSingleton<DatabaseContainer>();
builder.Services.AddHostedService(services => services.GetRequiredService<DatabaseContainer>());
Resolve the hosted service and pass the connection string to your database context:
builder.Services.AddDbContext<MyDbContext>((services, options) =>
{
var databaseContainer = services.GetRequiredService<DatabaseContainer>();
options.UseSqlServer(databaseContainer.GetConnectionString());
});
This example uses .NET's hosted service to leverage Testcontainers into the application start. By overriding the database context's OnModelCreating(ModelBuilder), this approach even takes care of creating the database schema and seeding data via Entity Framework while developing and testing.
3. Running inside a container
In some use cases, it might be necessary or a good approach to run the application out-of-process and inside a container. This increases the level of abstractions and removes the direct dependencies to the application. The services are only available through their public API (e.g. HTTP(S) endpoint).
The configuration follows the same approach as 1. Use environment variables to configure the application running inside a container. Testcontainers builds the necessary container image and takes care of the container lifecycle.
_container = new TestcontainersBuilder<TestcontainersContainer>()
.WithImage(Image)
.WithNetwork(_network)
.WithPortBinding(HttpsPort, true)
.WithEnvironment("ASPNETCORE_URLS", "https://+")
.WithEnvironment("ASPNETCORE_Kestrel__Certificates__Default__Path", _certificateFilePath)
.WithEnvironment("ASPNETCORE_Kestrel__Certificates__Default__Password", _certificatePassword)
.WithEnvironment("ConnectionStrings__DefaultConnection", _connectionString)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(HttpsPort))
.Build();
This example sets up all necessary Docker resources to spin up a throwaway out-of-process test environment.

how to add file logging to c# graph api notification app (switch or extend Ilogger to log infos to a log-File)

i have started in the last few weeks working (or trying it) Simple MVC-App for notifications.
I can log informations to console but how can I add easy an file logger for the notification function for http-requests?.
From the scratch implementing with very much work I can realize it, but without any supporting functions for filelogging. I found the Ilogger to log in the Console. But is there an easy way to switch Ilogger from logging to console logging to file?
public class NotificationsController : ControllerBase
{
private readonly MyConfig config;
private readonly ILogger<NotificationsController> _logger;
public NotificationsController(MyConfig config, ILogger<NotificationsController> logger)
{
this.config = config;
_logger = logger;
}
[HttpGet]
public ActionResult<string> Get()
{
...
string Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
_logger.LogInformation(Message);
...
The example I modified is fromn here:
https://learn.microsoft.com/de-de/learn/modules/msgraph-changenotifications-trackchanges/5-exercise-change-notification
https://github.com/microsoftgraph/msgraph-training-changenotifications/tree/live
Thank you for reaching out. AS.NET Core doesn't include a logging provider for writing logs to files, see documentation on logging in .NET Core and ASP.NET. To write logs to files, consider using a third party logging provider.
Let me know whether this helps and if you have further questions.

How to fix 500 Internal Server Error for POST integration tests using TestServer and Antiforgery? ASP.NET Core

I've got a working ASP.NET Core 2.2 implementation that utilizes both MVC and API controllers, and I'm putting together an integration test project to cover everything that has already been tested manually - the basic crud, mostly. Everything works except the tests that use PostAsync to POST data. These tests always get a 500 Internal Server Error as a response from the client, and I cannot for the life of me figure out why. Oi!
The TestServer setup is a pretty standard approach that I've seen on many different blogs and articles. A TestStartup class extends the standard Startup, and overrides the configuration to use in-memory database with seed data. My test fixture base then uses the TestStartup to create a server, and a client, and has a method, which I know works fine, to extract the antiforgery token and add it to forms and headers. All other tests verifying all other aspects of CRUD are working, proving that the seeded data can be retrieved, via both MVC and API calls.
The POST calls that eventually fail with a 500 Internal Server Error do make it into the controller, and the subsequent repository, just fine. With all these aspects in place, I've yet to be able to see the source of the 500.
[Fact]
public async void CreatePost_ShouldReturnViewWithNewlyCreatedLabelData()
{
// Arrange
var formData = await EnsureAntiForgeryTokenOnForm(new Dictionary<string, string>()
{
{ "Name", TestDataGraph.Labels.LabelNew.Name },
{ "WebsiteUrl", TestDataGraph.Labels.LabelNew.WebsiteUrl }
});
// Act
var response = await Client.PostAsync("/labels/create", new FormUrlEncodedContent(formData));
// Assert
Assert.Equal(HttpStatusCode.Found, response.StatusCode);
Assert.Equal("/Labels", response.Headers.Location.ToString());
}
This is a simple example test in Xunit that attempts to validate the creation of a new simple object, type Label, via MVC route, which follows the standard path format, having been scaffolded. This test will make it into the controller, and its repository, but the response will be a 500 Internal Server Error.
Could I have missed something important in Startup? Any ideas for finding further details about this failure? Thanks in advance! I can post more code or details if they will be helpful.
Try adding trace logging... Trace logging will display activity in the .Net Core framework.
...
public static ILogger<ConsoleLoggerProvider> AppLogger = null;
public static ILoggerFactory loggerFactory = null;
//
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging(builder => builder
.AddConsole()
.AddFilter(level => level >= LogLevel.Trace)
);
loggerFactory = services.BuildServiceProvider().GetService<ILoggerFactory>();
AppLogger = loggerFactory.CreateLogger<ConsoleLoggerProvider>();
...
An example trace log:
trce: Microsoft.AspNetCore.Mvc.Razor.Internal.RazorViewCompiler[7]
Could not find a file for view at path '/Views/Home/_Layout.cshtml'.
After you have resolved the issue, change the LogLevel to a more appropriate value.

Routes in AspNetCore.TestHost depend on Startup.cs location?

I am trying to test my ASP.NET Core Web Application with Microsoft.AspNetCore.TestHost. It works fine this way (result has status 200):
var server = new TestServer(new WebHostBuilder().UseStartup<Startup>());
var client = server.CreateClient();
var result = await client.GetAsync(someRequestUrl);
In this case the real Startup class from the API project is used.
However, I don't want to use the real Startup class in my integration test. The main reason is the need to mock some stuff that gets wired during application startup. For example, the database server to be used. It can be done in a very elegant way by defining a virtual method in Startup.cs:
public virtual void SetupDbContext(IServiceCollection services)
{
services.AddDbContext<TbsDb>(options =>
options.UseSqlServer("someConnectionString"));
}
Then I create a new class, which inherits from the original Startup class and overrides this method to use Sqlite, in-memory database or whatever:
public override void SetupDbContext(IServiceCollection services)
{
services.AddDbContext<TbsDb>(
options => options.UseSqlite("someConnectionString"));
}
This also works well with TestHost if the new class is in the same API project.
Obviously, I don't want this class which is used for testing to be there. But if I move it to integration tests project and create a TestServer there, the same test fails because the result has status 404. Why is it happening? It still inherits from the Startup class, which is in the API project. Thus I expect all the routes to work the same no matter where the TestStartup class is. Can it be solved somehow?

Using Startup class in ASP.NET5 Console Application

Is it possible for an ASP.NET 5-beta4 console application (built from the ASP.NET Console project template in VS2015) to use the Startup class to handle registering services and setting up configuration details?
I've tried to create a typical Startup class, but it never seems to be called when running the console application via dnx . run or inside Visual Studio 2015.
Startup.cs is pretty much:
public class Startup
{
public Startup(IHostingEnvironment env)
{
Configuration configuration = new Configuration();
configuration.AddJsonFile("config.json");
configuration.AddJsonFile("config.{env.EnvironmentName.ToLower()}.json", optional: true);
configuration.AddEnvironmentVariables();
this.Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.Configure<Settings>(Configuration.GetSubKey("Settings"));
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<ApplicationContext>(options => options.UseSqlServer(this.Configuration["Data:DefaultConnection:ConnectionString"]));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(minLevel: LogLevel.Warning);
}
}
I've tried to manually create the Startup class in my Main method, but this doesn't seem like the right solution and hasn't so far allowed me to configure the services.
I'm assuming there's some way for me to create a HostingContext that doesn't start up a web server but will keep the console application alive. Something along the lines of:
HostingContext context = new HostingContext()
{
ApplicationName = "AppName"
};
using (new HostingEngine().Start(context))
{
// console code
}
However so far the only way I can get this to work is if I set the HostingContext.ServerFactoryLocation to Microsoft.AspNet.Server.WebListener, which starts up the web server.
What you're looking for is the right idea, but I think you'll need to back up a moment.
Firstly, you may have noticed that your default Program class isn't using static methods anymore; this is because the constructor actually gets some dependency injection love all on its own!
public class Program
{
public Program(IApplicationEnvironment env)
{
}
public void Main(string[] args)
{
}
}
Unfortunately, there aren't as many of the services you're used to from an ASP.NET 5 hosting environment registered; thanks to this article and the IServiceManifest you can see that there's only a few services available:
Microsoft.Framework.Runtime.IAssemblyLoaderContainer
Microsoft.Framework.Runtime.IAssemblyLoadContextAccessor
Microsoft.Framework.Runtime.IApplicationEnvironment
Microsoft.Framework.Runtime.IFileMonitor
Microsoft.Framework.Runtime.IFileWatcher
Microsoft.Framework.Runtime.ILibraryManager
Microsoft.Framework.Runtime.ICompilerOptionsProvider
Microsoft.Framework.Runtime.IApplicationShutdown
This means you'll get the joy of creating your own service provider, too, since we can't get the one provided by the framework.
private readonly IServiceProvider serviceProvider;
public Program(IApplicationEnvironment env, IServiceManifest serviceManifest)
{
var services = new ServiceCollection();
ConfigureServices(services);
serviceProvider = services.BuildServiceProvider();
}
private void ConfigureServices(IServiceCollection services)
{
}
This takes away a lot of the magic that you see in the standard ASP.NET 5 projects, and now you have the service provider you wanted available to you in your Main.
There's a few more "gotchas" in here, so I might as well list them out:
If you ask for an IHostingEnvironment, it'll be null. That's because a hosting environment comes from, well, ASP.Net 5 hosting.
Since you don't have one of those, you'll be left without your IHostingEnvironment.EnvironmentName - you'll need to collect it from the environment variables yourself. Which, since you're already loading it into your Configuration object, shouldn't be a problem. (It's name is "ASPNET_ENV", which you can add in the Debug tab of your project settings; this is not set for you by default for console applications. You'll probably want to rename that, anyway, since you're not really talking about an ASPNET environment anymore.)

Categories