Over the past few weeks, I’ve been learning about testing within the C# .NET eco system, as this is and still my weakest area. I’ve been reading about the different types of testing from unit testing to integration testing, I decided to dive deep and go straight for integration testing in this article.
Before going any further, let me give you some context I am looking to build an API that will replace this current blogging platform. I’ve decided to build the blog backend API in two technology stacks One in .NET 6 (Latest Version) using Entity Framework and another version using Node.js. with TypeORM and possibly Mongoose, My reasoning behind this is to have a project to work on that allows me to learn both the languages and plus my current hosting provider doesn’t allow .NET Core apps.
I am following clean architecture approach for the C# version of the Blog API as well as the CQRS (Command and Query Responsibility Segregation) Pattern.
The Integration Test
with some context out of the way let me explain what my first test is about. For my first integration test I would like to be able to register a new user to the system and for that user to be persisted into my test database.
Creating the BlogAppFactory class
My BlogAppFactory class I created within the root of my XUnit test project, this class is responsible for the startup of my test project and allows me to de-couple of the live PostgreSQL database to a test PostgreSQL database that gets created on the fly.
I decided to not use an in memory test db (Since I’m learning best practices for testing), for the following reasons:
- In memory database providers reduce the reliability and scope of tests.
- Not an exact copy of the real database server
- A good deal of the LINQ surface area can’t be translated into SQL
- No Database transaction (ACID)
I good post that I came across was this: https://jimmybogard.com/avoid-in-memory-databases-for-tests/
Writing the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
public class BlogAppFactory : WebApplicationFactory<Program> { private readonly string _sessionId = Guid.NewGuid().ToString(); protected override void ConfigureWebHost(IWebHostBuilder builder) { base.ConfigureWebHost(builder); builder.ConfigureServices(services => { var servicesToRemove = new[] { typeof(DbContextOptions<BlogDbContext>) }; foreach (var serviceType in servicesToRemove) { var service = services.SingleOrDefault(d => d.ServiceType == serviceType); services.Remove(service); } var connectionString = $"Host=localhost;Database=dbjoeBlog-test-{_sessionId};Username=postgres;Password=80min700"; services.AddDbContext<BlogDbContext>(options => options.UseNpgsql(connectionString)); var sp = services.BuildServiceProvider(); using (var scope = sp.CreateScope()) { var scopedServices = scope.ServiceProvider; var blogDb = scopedServices.GetRequiredService<BlogDbContext>(); blogDb.Database.EnsureCreated(); } }); } protected override void Dispose(bool disposing) { using var scope = Services.CreateScope(); var sp = scope.ServiceProvider; var ctx = sp.GetRequiredService<BlogDbContext>(); ctx.Database.EnsureDeleted(); } } |
My BlogAppFactory class inherits from WebApplicationFactory using Program as the startup file in order to allow the Program.cs file to be used in this class I needed to add the following at the bottom.
1 2 |
// Make the implicit Program class public so test projects can access it public partial class Program { } |
I can then override the ConfigureWebHost to perform actions before my test project gets built. In this instance I am removing my live BlogDbContext from the injected services, I am then re-injecting my DBContext service with a new connection string. I am then making sure that this new test database gets created on the fly by using the EnsureCreated() method.
I then have a Dispose method to remove the database once all tests have run. (there is no guarantee that this will work all the time)
1 2 3 4 5 6 7 |
protected override void Dispose(bool disposing) { using var scope = Services.CreateScope(); var sp = scope.ServiceProvider; var ctx = sp.GetRequiredService<BlogDbContext>(); ctx.Database.EnsureDeleted(); } |
My Test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
[Collection(nameof(PostgresCollection))] public class PostUserTest { private readonly BlogAppFactory _factory; public PostUserTest(BlogAppFactory factory) { //_factory = factory.withwebhostbuilder(builder => builder.configureservices(services => {}) _factory = factory; } [Fact] public async Task RegisterNewUser() { using (var scope = _factory.Services.CreateScope()) { var authService = scope.ServiceProvider.GetRequiredService<IAuthenticationService>(); RegistrationRequest user = new RegistrationRequest() { FirstName = "TestUser", LastName = "TestUser", ProfileImageUri = "string", DisplayName = "testdb", Email = "test@test.com", Password = "password" }; await authService.RegisterAsync(user); } } } |
At the top of my test class I have a [Collection] Xunit attribute, this allows me to tie my test class to my fixture, this allows me to share my test database that I created earlier with my tests. More information can be found here: https://xunit.net/docs/shared-context#collection-fixture
So I needed to create a new class in the root of my Xunit test project called PostgresCollection
1 2 3 4 |
[CollectionDefinition(nameof(PostgresCollection))] public class PostgresCollection : ICollectionFixture<BlogAppFactory> { } |
now going back to my test, I have a constructor to call my BlogAppFactory and this has been assigned to a backing field private readonly BlogAppFactory _factory;
within the Fact section of my test I’ve asked DI can I have the service called IAuthenticationService and assign it to the variable authService, I am then creating a new user of type RegistrationRequest and then finally I am calling the RegisterAsync and passing in the new user.
The above test may not be perfect, and I am more than likely missing some things, but as I continue to learn more about testing I will refactor and update this blog on my progress.