ASP.NET Core Integration tests with dotnet-testcontainers - c#

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.

Related

Changing RedisCacheOptions after registering the StackExchangeRedisCache middleware

I'm trying to use Redis in my Asp.Net Core project. I added the packages StackExchange.Redis and Microsoft.Extensions.Caching.StackExchange.Redis to my project and the following code to register the middleware:
services.AddStackExchangeRedisCache(options =>
{
// do config
});
So far that works fine. But in this project Configuration settings are provided by a custom ConfigurationService which is pulling them from an external source. If changes in any of the configuration settings concerning Redis are detected I need to be able to reconfigure the StackExchangeRedisCache middleware.
How would I do that?
If I get the question right, does the case is... The project configuration setting was customize to fetch from another source by some interval ? Therefore, the setting on redis could change by time to time ?
If that's the case, I think we got to drop usage of AddStackExchangeRedisCache extensions. If you want to keep using this, here is some problem that got to solve:
Remove RedisCache singleton instance that previously register in ServiceCollection.
Remove config for RedisCacheOptions previously register in ServiceCollection
Register new RedisCacheOptions and RedisCache in ServiceCollection
Re-initialize ServiceProvider somehow
These step should be done atomicity, otherwise, exceptions likely to raise.
Quite cumbersome... does it ?
Another approach was to create a placeholder for IDistributedCache instance, register it as singleton and update the IDistributedCache instance if change was detected from the Configuration.
// Place holder, and register it as singleton
public class CacheAccessor : IDisposable
{
private RedisCache RedisCache { get; set; }
public void SetRedisCache(IOptions<RedisCacheOptions> options)
{
RedisCache?.Dispose();
RedisCache = new RedisCache(options);
}
public IDistributedCache LatestRedisCache => RedisCache;
public void Dispose()
{
RedisCache?.Dispose();
}
}
// Register it
services.AddSingleton<CacheAccessor>();
// Initialize it for the first time (write some extension or put it somewhere in program.cs would be nice.
// Run after the webhost instance got built and before it run)
// host was an instance of IHost
using var scope = host.Services.CreateScope();
var serviceProvider = scope.ServiceProvider;
serviceProvider.GetRequiredService<CacheAccessor>().SetRedisCache(Pass the option here).
// By time to time, if the config changed, extract the option and SetRedisCache again.
// And of course, get access to the cache would be difference
public IActionResult TestMethod([FromServices] CacheAccessor accessor)
{
var cache = accessor.LatestRedisCache;
}

Implement full logging in Integration Test

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.

Injecting Connection String into DbContext Class using Autofac in a TopShelf Service (.Net Framework)

I'm creating a service that needs to reference legacy DLLs for our ERP system so unfortunately .NET framework is how I need to do this. I tried using a Core Worker Service but my calls to the DLLs won't work.
I like using TopShelf and looked up DI to use with this and Autofac seems to be the way to go and it has TopShelf support. I don't like keeping any settings in our config files so that it's easier to deploy to other environments. We have a whole system that keeps settings tied to the environment you are in (dev, test, prod, etc...). In Core apps I simply inject the connection string in the startup and all is well. In this app I want to inject the connection string when I start the service and be able to spin up a DbContect class at anytime and have it use that connection string.
Since I scaffold my data layer and didn't want to modify the generated DbContext, so I created another partial class (MyContext.Custom.cs) with a constructor that allows me to pass in the connection string
public MyContext(string name) : base(name)
{ }
In my Program.cs I'm adding in an Autofac container to TopShelf
HostFactory.Run(serviceConfig =>
{
serviceConfig.UseAutofacContainer(container);
// etc...
}
The container is being built in a function, I tried following many examples but I can't seem to get my constructor with the parameter to be the one that is used. Here are a few of the ways I've tried. Each method produces no error but it runs my default constructor where it is looking for the named connection string in the config file.
static IContainer BuildContainer()
{
string myConnectionString = AppConfig.Instance.Settings["MyConnectionString"];
var builder = new ContainerBuilder();
//builder.RegisterType<MyContext>()
// .UsingConstructor(typeof(string))
// .WithParameter("name", myConnectionString)
// .InstancePerLifetimeScope();
//builder.Register(c => new MyContext(myConnectionString)).As<MyContext>();
//.WithParameter("name", myConnectionString);
//builder.RegisterType<MyContext>()
// .WithParameter("name", myConnectionString)
// .AsSelf();
//.InstancePerLifetimeScope();
builder.Register(c => new MyContext(myConnectionString)).AsSelf();
builder.RegisterType<MainService>();
return builder.Build();
}
I've tried each variation with .As and .AsSelf(). I've put in the .InstancePerLifetimeScope() and also left it out. I'm not really sure what to set the scope to in this case but figured it should work with lifetime. No matter what I've tried I can't seem to get it to use my constructor.
If I've left out any information feel free to ask and I can fill it in. Hopefully someone has done this. I guess I could pass in the connection string everytime I instantiate a DbContext but I was hoping to get it to work like a Core app which is so much nicer.
thanks
EDIT:
Adding code to show how I'm using my DbContext
public bool Start()
{
using(var db = new MyContext())
{
var warehouse = (from w in db.Warehouses
where w.WarehouseCode == "ABCD"
select w).FirstOrDefault();
}
// other code...
}

How to use settings from integration testing project in the Startup class of api project?

I'm trying to write integration tests for web api, I have two appsettings.json files one for api project and other for integration tests. I want to use the values(azure storage connection strings) from integration testing project in Startup class of web api.
I have tried creating CustomWebApplicatonFactory it didn't work since the Startup class of web api gets settings like shown below.
//Configure services method in Startup class
public virtual void ConfigureServices(IServiceCollection services)
{
var settings = ConfigurationProvider.GetConfiguration();
services.TryAddSingleton(settings);
services.AddHttpClient();
var azureTableStorageConnectionString =
settings["AzureMainStoreConnectionStringSecretName"];
//Other Startup related code
}
I want to change the value of "azureTableStorageConnectionString" from my integration test project. Help and suggestions are much appreciated.
First of all, instead of using the static function ConfigurationProvider.GetConfiguration() inject IConfiguration into your Startup class. The host that is usually defined in the Program class builds that for you so you can inject it.
Then instead of using WebApplicationFactory<TStartup> you can build a test-host yourself like this.
[Fact]
public async Task LoadWeatherForecastAsync()
{
var webHostBuilder = new WebHostBuilder()
.UseContentRoot(AppContext.BaseDirectory)
.ConfigureAppConfiguration(builder =>
{
builder.Sources.Clear();
builder.SetBasePath(AppContext.BaseDirectory);
// this is an appsettings file located in the test-project!
builder.AddJsonFile("appsettings.Testing.json", false);
})
.UseStartup<Startup>();
var host = new TestServer(webHostBuilder);
var response = await host.CreateClient().GetAsync("weatherforecast");
Assert.True(response.IsSuccessStatusCode);
}
I've created a sample on github. You can clone it and try it out.

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?

Categories