6

ASP.NET Core 5 & EntityFramework Core: Clean, clear and fluent integration t...

 3 years ago
source link: https://anthonygiretti.com/2021/04/17/asp-net-core-5-entityframework-core-clean-clear-and-fluent-integration-tests-with-calzolari-testserver-entityframework-fluentassertion-web-and-xunit/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
ASP.NET Core 5 & EntityFramework Core: Clean, clear and fluent integration tests with Calzolari.TestServer.EntityFramework, FluentAssertion.Web and xUnit
2021-04-17 by anthonygiretti

Introduction

Not long ago I was asked to do integration tests with ASP.NET Core and Entity Framework Core and I was confronted with some difficulties, in particular the fact of testing Entity Framework Core in integration? I have not found a simple tutorial to create clear and simple tests and especially how to get around the limitations of Sql in memory when using EntityFramework Core. So I had to find work arounds with SqlLite to do my tests while making sure that there are no data collisions in each test. In addition, I wanted to make sure that I was using AAA (Arrange, Act, Assert) in such a way as to maintain optimal ideal readability. So I had the idea to create the Calzolari.TestServer.EntityFramework library which encapsulates all the management of EntityFramework Core in integration, this library also uses Flurl for writing Http clients in a fluid way and FakeBearerToken allowing to pass tokens in the simplest way in endpoints protected by JWTs. In this article I will also show how to use Calzolari.TestServer.EntityFramework in conjunction with FluentAssertions.Web which is an assertion library for the web and xUnit in order to perform really clean integration tests! I used as well AutoFixture to facilitate somme Arrange.

Note that Parallelism MUST BE deactivated while testing, the database is created and removed after each test which avoids any data collision, the DbContext is reinstancianted between each test. It’s not recommended for large databases.

Test scenario

We’ll consider the following scenario, an endpoint protected by a JWT which gets a country by its Id, and the second one protected with a JWT that must contains the Admin role which creates a country:

using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.Linq; using Calzolari.WebApi.Database; using Calzolari.WebApi.Models; using Microsoft.AspNetCore.Authorization;

namespace Calzolari.WebApi.Controllers { [ApiController] [Route("api/[controller]")] public class CountryController : ControllerBase { private readonly DemoDbContext _dbContext;

public CountryController(DemoDbContext dbContext) { _dbContext = dbContext; }

[Authorize] [HttpGet] public IEnumerable<Country> Get() { return _dbContext.Countries; }

[Authorize(Roles = "Admin")] [HttpPost] public IActionResult Post([FromBody] Country country) { _dbContext.Countries.Add(country); _dbContext.SaveChanges();

return CreatedAtAction(nameof(GetById), new { id = country.CountryId }, country); } } }

Setup your integration test project

Install first the following packages, for example in command line in the Package Manager console:

Install-Package Calzolari.TestServer.EntityFramework
Install-Package AutoFixture
Install-Package FluentAssertions.Web
Install-Package xunit
Install-Package xunit.runner.visualstudio
Install-Package Microsoft.AspNetCore.Mvc.Testing
Install-Package Microsoft.NET.Test.Sdk

Don’t forget to import as reference the ASP.NET Core project you want to test.

Create a dedicated Startup.cs file for your integration tests

I strongly suggest to create specific Startup.cs file (name it TestStartup.cs for example) for better clarity, don’t use the Startup.cs file from the web project you want to test.

Then :

  • Add the method AddApplicationPart which takes in parameter any class of your webapplication (for example the Startup class), this is needed to retrieve controllers and register them in the integration test project
  • Register the DbContext with the following method: AddIntegrationTestDbContext, this is needed for integration test to be working with EF Core, behind the scene it register your DbContext with SQLite Database
  • Register the fake bearer token authentication with the AddFakeBearerToken method, this is needed to create an identity with a fake token while performing integration tests

It should look like this:

using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Calzolari.TestServer.EntityFramework.Database.EF; using Calzolari.TestServer.EntityFramework.FakeBearerToken; using Calzolari.WebApi.Database;

namespace Calzolari.WebApi.Tests.Common { public class TestStartup { public TestStartup(IConfiguration configuration) { Configuration = configuration; }

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers().AddApplicationPart(typeof(Startup).Assembly); services.AddIntegrationTestDbContext<DemoDbContext>() .AddFakeBearerToken(); }

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting();

app.UseAuthentication();

app.UseAuthorization();

app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } }

Create a Web factory to create the test server

Inherit from FlurlWebFactory, generic T can be any class of your webapplication (for example the Startup class, the reason why is explanied in a previous post here: TestServer & ASP.NET Core 5: Fix “System.InvalidOperationException : Solution root could not be located using application root” with a custom Startup file – Anthony Giretti’s .NET blog), then override CreateHostBuilder and use your TestStartup.cs file like this:

using Calzolari.TestServer.EntityFramework.Flurl; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting;

namespace Calzolari.WebApi.Tests.Common { public class TestFactory : FlurlWebFactory<Startup> { protected override IHostBuilder CreateHostBuilder() { return Host.CreateDefaultBuilder().ConfigureWebHost((builder) => { builder.UseStartup<TestStartup>(); }); } } }

Create a xUnit collection definition

A collection definition allows injection dependency (Singleton lifetime) within your test classes. The following example shows how to register a collection named “AssemblyFixture” that allows to pass into your test classes the TestFactory we defined just before:

using Xunit;

namespace Calzolari.WebApi.Tests.Common { [CollectionDefinition("AssemblyFixture")] public class AssemblyFixture : ICollectionFixture<TestFactory> { } }

Create a base test class

Inherit from IntegrationTestBase<TDbContext, T>, TDbContext is your DbContext and T is the same class that you defined as generic parameter on FlurlWebFactory when you wrote your web factory, then, add [Collection(“AssemblyFixture”)] attribute on your base test class. This is here where you can add AutoFixture or anything else that you need for your all your tests. Your base test class should look like this:

using AutoFixture; using Calzolari.TestServer.EntityFramework.Database.EF; using Calzolari.WebApi.Database; using Xunit;

namespace Calzolari.WebApi.Tests.Common { [Collection("AssemblyFixture")] public class DemoTestBase : IntegrationTestBase<DemoDbContext, Startup> { protected readonly IFixture Fixture;

public DemoTestBase(TestFactory factory) : base(factory) { Fixture = new Fixture(); } } }

Write your tests

Create a test class and inherit from the base test class. Pass the web factory by injection dependency and write your tests. I suggest to use FluentAssertions for your assertions

To feed the database use the method Arrange like this

var countries = Fixture.Build<Country>()
                       .CreateMany(3);
  
Arrange(dbContext =>
{
    dbContext.Countries.AddRange(countries);
});

If you expect an auto incremented Id, it’s filled automatically like this:

var country = Fixture.Create<Country>();

Arrange(dbContext => { dbContext.Countries.Add(country); });

// country.CountryId CountryId is filled

Create a fake token to be authenticated while calling the endpoint to test, sub is the claim that represents the identity within a JWT:

var token = new
{
    sub = "Anthony Giretti"
};

Then call your System Under Test (SUT):

var response = await BASE_REQUEST.Route(BaseRoute).FakeToken(token).GetAsync();

And assert the result by using FluentAssertions.Web:

response.ResponseMessage
	.Should()
	.Be200Ok()
	.And
	.BeAs(countries);

The response is typed as IFlurlResponseMessage, whichs exposes the native HTTP ResponseMessage , that’s why I had to write response.ResponseMessage to get the HTTP response.

The test class should look like this:

using System.Collections.Generic; using System.Threading.Tasks; using AutoFixture; using Calzolari.TestServer.EntityFramework.Flurl; using Calzolari.WebApi.Models; using Calzolari.WebApi.Tests.Common; using FluentAssertions; using Flurl.Http; using Xunit;

namespace Calzolari.WebApi.Tests.CountryControllerTests { public class GetTests : DemoTestBase { private const string BaseRoute = "/api/country";

public GetTests(DemoFactory factory) : base(factory) { }

[Fact] public async Task When_route_is_correct_and_jwt_is_set_and_database_fed_should_return_Ok_and_collection_of_country() { // Arrange var countries = Fixture.Build<Country>() .CreateMany(3);

Arrange(dbContext => { dbContext.Countries.AddRange(countries); });

var token = new { sub = "Anthony Giretti" };

// Act var response = await BASE_REQUEST.Route(BaseRoute).FakeToken(token).GetAsync();

// Assert response.ResponseMessage .Should() .Be200Ok() .And .BeAs(countries); }

[Fact] public async Task When_route_is_not_correct_should_return_NotFound() { // Arrange var token = new { sub = "Anthony Giretti" }; // Act var response = await BASE_REQUEST.Route("wrongroute").FakeToken(token).GetAsync();

// Assert response.ResponseMessage.Should().Be404NotFound(); }

[Fact] public async Task When_route_is_correct_and_jwt_is_set_and_database_not_fed_should_return_Ok_and_empty_collection_of_country() { // Arrange var token = new { sub = "Anthony Giretti" }; // Act var response = await BASE_REQUEST.Route(BaseRoute).FakeToken(token).GetAsync();

// Assert response.ResponseMessage .Should() .Be200Ok() .And .Satisfy<IEnumerable<Country>>(model => { model.Should().BeNullOrEmpty(); }); } [Fact] public async Task When_route_is_correct_and_jwt_is_missing_should_return_Unauthorized() { // Arrange

// Act var response = await BASE_REQUEST.Route(BaseRoute).GetAsync();

// Assert response.ResponseMessage .Should() .Be401UnAuthorized() } } }

You can find more example here, especially with Authorization based on Roles on a POST endpoint:

using System.Threading.Tasks; using AutoFixture; using Calzolari.TestServer.EntityFramework.Flurl; using Calzolari.WebApi.Models; using Calzolari.WebApi.Tests.Common; using FluentAssertions; using Flurl.Http; using Xunit;

namespace Calzolari.WebApi.Tests.CountryControllerTests { public class PostTests : DemoTestBase { private const string BaseRoute = "/api/country";

public PostTests(DemoFactory factory) : base(factory) { }

[Fact] public async Task When_route_is_correct_and_authorization_well_setup_should_return_Created() { // Arrange var country = Fixture.Build<Country>().Without(c => c.CountryId).Create(); var token = new { sub = "Anthony Giretti", role = new[] { "Admin" } };

// Act var response = await BASE_REQUEST.Route(BaseRoute).FakeToken(token).PostJsonAsync(country);

// Assert response.ResponseMessage .Should() .Be201Created() .And.BeAs(new { country.CountryName, country.Description }); }

[Fact] public async Task When_route_is_correct_and_role_missing_should_return_Forbidden() { // Arrange var country = Fixture.Create<Country>(); var token = new { sub = "Anthony Giretti" };

// Act var response = await BASE_REQUEST.Route(BaseRoute).FakeToken(token).PostJsonAsync(country);

// Assert response.ResponseMessage .Should() .Be403Forbidden(); }

[Fact] public async Task When_route_is_correct_and_role_is_wrong_should_return_Forbidden() { // Arrange var country = Fixture.Create<Country>(); var token = new { sub = "Anthony Giretti", role = new[] { "WrongRole" } };

// Act var response = await BASE_REQUEST.Route(BaseRoute).FakeToken(token).PostJsonAsync(country);

// Assert response.ResponseMessage .Should() .Be403Forbidden(); }

[Fact] public async Task When_route_is_correct_and_token_is_missing_should_return_Forbidden() { // Arrange var country = Fixture.Create<Country>();

// Act var response = await BASE_REQUEST.Route(BaseRoute).PostJsonAsync(country);

// Assert response.ResponseMessage .Should() .Be401Unauthorized(); } } }

Conclusion

This post aimed to show how you could test your ASP.NET Core application which uses EntityFramework Core, as you can see, all the complexity brought by EntityFramework Core in integration testing has been encapsulated. Don’t forget it’s not recommanded for larage database, on my end, I’m testing my microservices like this.

I hope you liked this post, and if you have any suggestion, for example, adding new features in Calzolari.TestServer.EntityFramework, feel free to send me an email :).

Like this:

Loading...

Related posts


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK