DEV Community

loading...
Cover image for .NET Core and integration tests

.NET Core and integration tests

lhersey profile image Hersey Aguilar ・8 min read

Integration test for .NET Core - Clean architecture

This project has an example of how to write clean and maintainable test with xUnit, TestServer, FluentAssertions and more!

The production code

First lets look at all the production code... (Database schema configuration, Hashing, JWT Configuration, Token Generation, Validation).

This is the folder structure:

CleanVidly/
    src/
        CleanVidly/
            Controllers/        <-- All the controllers with their DTOS and validators
            Extensions/         <-- Extension methods for pagination and other things
            Core/               <-- Abstractions and Entities
            Mapping/            <-- AutoMapper configuration 
            Persistance/        <-- All repositories and database schema configuration (Migrations here too)
            Infraestructure/    <-- Helpers classes for generate JWT and password hashing
    tests/
        CleanVidly.IntegrationsTest/
            Controllers/        <-- Endpoints tests
            Extensions/         <-- Extensions for testing
            Helpers/            <-- Helpers for testing
Enter fullscreen mode Exit fullscreen mode

Everything will make sense! Just be patient! Before testing I want to share with you the parts of the production code.

Is a simple app with a clean and simple structure, have all REST endpoints for 'Categories' and 'Roles', besides an endpoint for JWT login.

Why this solution is not splitted into multiple projects? (View/BusinessLogic/Data)

Please see this post of Mosh Hamedani about this.

The Inversion dependency principle is in practice even with just one project The Controller layer depend upon abstractions on Core layer, and Persistance depend on Core too. All depent on the dependency direction, no folder, no projects.

JWT Token generation and validation with roles verification.

Setup the token services and authorization middleware on Startup.cs with:

private void GetAuthenticationOptions(AuthenticationOptions options)
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}

private void GetJwtBearerOptions(JwtBearerOptions options)
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,

        ValidIssuer = Configuration["Jwt:ValidIssuer"],
        ValidAudience = Configuration["Jwt:ValidAudience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:SecurityKey"]))
    };
}
Enter fullscreen mode Exit fullscreen mode

Use them inside ConfigureServices method with:

services
    .AddAuthentication(GetAuthenticationOptions)
    .AddJwtBearer(GetJwtBearerOptions);
Enter fullscreen mode Exit fullscreen mode

And add this in the Configure method

app.UseAuthentication();
app.UseMvc();
Enter fullscreen mode Exit fullscreen mode

Generate the token on each valid login with:

public string GenerateToken(User user)
{

    var claims = new List<Claim>() {
        new Claim(JwtRegisteredClaimNames.NameId, user.Id.ToString()),
        new Claim(JwtRegisteredClaimNames.UniqueName, user.Email),
        new Claim("name", user.Name),
        new Claim("lastname", user.Lastname),
        new Claim("joinDate", user.JoinDate.ToString("dd/MM/yyyy H:mm")),
    };

    var roles = user.UserRoles.Select(ur => new Claim("roles", ur.Role.Description));
    claims.AddRange(roles);

    return GetJwtToken(claims);
}

private string GetJwtToken(IEnumerable<Claim> claims)
{
    var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
    var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha512);

    var tokeOptions = new JwtSecurityToken(
        issuer: validIssuer,
        audience: validAudience,
        claims: claims,
        expires: DateTime.UtcNow.AddHours(24),
        signingCredentials: signinCredentials
    );

    return new JwtSecurityTokenHandler().WriteToken(tokeOptions);
}
Enter fullscreen mode Exit fullscreen mode
Validation

I prefer to use FluentValidation to avoid Data anotations and because is so much powerful

public class UserValidator : AbstractValidator<SaveUserResource>
{
    public UserValidator()
    {
        RuleFor(u => u.Name).NotEmpty().MinimumLength(4).MaximumLength(32);
        RuleFor(u => u.Lastname).NotEmpty().MinimumLength(4).MaximumLength(32);
        RuleFor(u => u.Email).NotEmpty().MinimumLength(4).MaximumLength(128).EmailAddress();
        RuleFor(u => u.Roles).NotEmpty();
    }
}
Enter fullscreen mode Exit fullscreen mode

It is cleaner and respects the Single responsability principle! Supports regular expressions, async validation and more.

To validate all Dtos automaticly on each request, add a line of code on our Startup class

services
    .AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
    .AddFluentValidation(fvc => fvc.RegisterValidatorsFromAssemblyContaining<Startup>());
Enter fullscreen mode Exit fullscreen mode

This line tells to search all the validators on the current assembly.

On each request, if add a dto as a parameter of an enpoint, it will check for a validator, validate it and if any errors, returns a 400 BadRequest with all the errors, cool right?

Database Schema

To configure all columns and tables I don't use data anotations, use pure FluentAPI but that OnModelCreating method was messy with so many code... So I use IEntityTypeConfiguration to create a configuration class for each model:

public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.Property(u => u.Email).HasMaxLength(128).IsRequired();
        builder.Property(u => u.Name).HasMaxLength(32).IsRequired();
        builder.Property(u => u.Lastname).HasMaxLength(32).IsRequired();
        builder.Property(u => u.Salt).HasMaxLength(128).IsRequired();
        builder.Property(u => u.Password).HasMaxLength(64).IsRequired();
        builder.Property(u => u.JoinDate).HasDefaultValueSql("GETDATE()").IsRequired();
    }
}
Enter fullscreen mode Exit fullscreen mode

And in OnModelCreating call this to read all the configuration classes on the current assembly:

modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
Enter fullscreen mode Exit fullscreen mode
Password Hash

To password hashing use these funcions:

public static byte[] CreateHash(byte[] salt, string valueToHash)
{
    using (var hmac = new HMACSHA512(salt))
    {
        return hmac.ComputeHash(Encoding.UTF8.GetBytes(valueToHash));
    }
}

public static byte[] GenerateSalt()
{
    using (var hmac = new HMACSHA512())
    {
        return hmac.Key;
    }
}

public static bool VerifyHash(string password, byte[] salt, byte[] actualPassword)
{
    using (var hmac = new HMACSHA512(salt))
    {
        var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
        return computedHash.SequenceEqual(actualPassword);
    }
}
Enter fullscreen mode Exit fullscreen mode

How setup integration test?

I prepare a configuration singleton instance to read the appSettings.Test.json on the test project, and no the normal appSettings from the source project.

private static IConfigurationRoot configuration;
private ConfigurationSingleton() { }
public static IConfigurationRoot GetConfiguration()
{
    if (configuration is null)
        configuration = new ConfigurationBuilder()
        .SetBasePath(Path.Combine(Path.GetFullPath("../../../")))
        .AddJsonFile("appsettings.Test.json")
        .AddEnvironmentVariables()
        .Build();


    return configuration;
}
Enter fullscreen mode Exit fullscreen mode

We need a DbContext to validate that each endpoint did what it is supposed to, so create a DbContextFactory:

public DbContextFactory()
{
    var dbBuilder = GetContextBuilderOptions<CleanVidlyDbContext>("vidly_db");

    Context = new CleanVidlyDbContext(dbBuilder.Options);
    Context.Database.Migrate(); //Execute migrations
}
Enter fullscreen mode Exit fullscreen mode

Sometimes, this context is not aware of the changes the API did, so we need to refresh the context:

public CleanVidlyDbContext GetRefreshContext()
{
    var dbBuilder = GetContextBuilderOptions<CleanVidlyDbContext>("vidly_db");
    Context = new CleanVidlyDbContext(dbBuilder.Options);

    return Context;
}
Enter fullscreen mode Exit fullscreen mode

The client created by TestServer sometimes is a little hard to work with, so the Request class help to make it a little easier.

public JwtAuthentication Jwt => new JwtAuthentication(ConfigurationSingleton.GetConfiguration());

//Returns this to chain with the REST endpoint ex: `request.AddAuth(token).Get("api/sensitiveData");`
public Request<TStartup> AddAuth(string token)
{
    this.client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
    return this;
}

/**
 * Also Get, post, put and delete methods
 */
Enter fullscreen mode Exit fullscreen mode

Also created a extension method to read the body of the response with more cleaner code

public static async Task<T> BodyAs<T>(this HttpResponseMessage httpResponseMessage)
{
    var bodyString = await httpResponseMessage.Content.ReadAsStringAsync();
    return JsonConvert.DeserializeObject<T>(bodyString);
}
Enter fullscreen mode Exit fullscreen mode

I just call

res.BodyAs<Employees[]>();
Enter fullscreen mode Exit fullscreen mode

And that's it! Now the tests...

How to create integration test correctly?

I always set my test to run on 'happy path', and for each test change what I need to change!

First on each class, implement these interfaces

public class CategoriesControllerPostTests : IClassFixture<Request<Startup>>, IClassFixture<DbContextFactory>, IDisposable
Enter fullscreen mode Exit fullscreen mode

The constructor will run BEFORE each test of the class and the IDisposable is to run the Dispose method AFTER each test,

The IClassFixture is to have a single context between tests, so we use to create the Request and DbcontextFactory instances, and create a constructor for them.

private readonly Request<Startup> request;
private readonly CleanVidlyDbContext context;
public CategoriesControllerPostTests(Request<Startup> request, DbContextFactory contextFactory)
{
    this.request = request;
    this.context = contextFactory.Context;
}
Enter fullscreen mode Exit fullscreen mode

We need to run each test on a clean state... The Dispose method cleans the database after each test.

public void Dispose()
{
    context.Categories.RemoveRange(context.Categories);
    context.SaveChanges();
}
Enter fullscreen mode Exit fullscreen mode

Now let's write our first integration test!

[Fact]
public async Task ShouldSave_Category_IfInputValid()
{
    //Send a request to /api/cateogires with that body
    await request.Post("/api/categories", new { Description = "My description" });

    //Select directly to the Db;
    var categoryInDb = await context.Categories.FirstOrDefaultAsync(c => c.Description == "My description");

    //Validate that is not null (Thanks FluentAssertions!)
    categoryInDb.Should().NotBeNull();
}
Enter fullscreen mode Exit fullscreen mode

But there is a problem with this implementation, the request will fail with status 401 Unauthorized... So we need to make a change...

[Fact]
public async Task ShouldSave_Category_IfInputValid()
{
    //Create user to generate JWT token
    var user = new User()
    {
        Email = "",
        Name = "",
        Lastname = "",
        Id = 1
    };

    //Generate token
    var Token = request.Jwt.GenerateToken(user);

    //Send a request to /api/cateogires with that body and token
    await request.AddAuth(Token)Post("/api/categories", new { Description = "My description" });

    //Select directly to the Db;
    var categoryInDb = await context.Categories.FirstOrDefaultAsync(c => c.Description == "My description");

    //Validate that is not null (Thanks FluentAssertions!)
    categoryInDb.Should().NotBeNull();
}
Enter fullscreen mode Exit fullscreen mode

But our test is getting fat and dirty... I prefer a diferent aproach... Let´s make some changes! Extract the request into another method and extract the Description and Token as a variable to change them depending on the test.
We got this:

private readonly Request<Startup> request;
private readonly CleanVidlyDbContext context;

private string Description;
private string Token;

public CategoriesControllerPostTests(Request<Startup> request, DbContextFactory contextFactory)
{
    this.request = request;
    this.context = contextFactory.Context;

    Description = "Valid Category"; //Initalize with some valid value!

    var user = new User()
    {
        Email = "",
        Name = "",
        Lastname = "",
        Id = 1
    };

    Token = request.Jwt.GenerateToken(user);
}

public void Dispose()
{
    context.Categories.RemoveRange(context.Categories);
    context.SaveChanges();
}

public Task<HttpResponseMessage> Exec() => request.AddAuth(Token).Post("/api/categories", new { Description = Description });

[Fact]
public async Task ShouldSave_Category_IfInputValid()
{
    //Don't change anything, this is the test of the happy path!
    await Exec(); 

    //Should be equal to the variable without change
    var categoryInDb = context.Categories.FirstOrDefault(c => c.Description == Description); 

    //Should found the category
    categoryInDb.Should().NotBeNull();
}
Enter fullscreen mode Exit fullscreen mode

Everything is good, but how to test other paths? What if no Description is provided? Will it respond with 400 BadRequest?

[Fact]
public async Task ShouldReturn_BadRequest400_IfDescription_LessThanFourCharacters()
{
    Description = "a"; //Just change what the test should supose to test and call Exec(); It is cleaner!

    var res = await Exec();
    var body = await res.BodyAs<ValidationErrorResource>(); //It supose to return a error model with the errors!

    res.StatusCode.Should().Be(HttpStatusCode.BadRequest); 
    body.Errors.Should().ContainKey("Description"); //Should return an error for Description (From FluentValidation!)
}
Enter fullscreen mode Exit fullscreen mode

It´s simple, clean, self explanatory and the test do exactly what it says, nothing less, nothing more!
The full class is:

private readonly Request<Startup> request;
private readonly ITestOutputHelper output;
private readonly CleanVidlyDbContext context;
private string Description;
private string Token;
public CategoriesControllerPostTests(Request<Startup> request, DbContextFactory contextFactory)
{
    this.request = request;
    this.context = contextFactory.Context;

    //Set before each test, remember... The constructor runs before each test
    Description = "Valid Category";  

    var user = new User()
    {
        Email = "",
        Name = "",
        Lastname = "",
        Id = 1
    };

    Token = request.Jwt.GenerateToken(user);

}

public void Dispose()
{
    context.Categories.RemoveRange(context.Categories);
    context.SaveChanges();
}

public Task<HttpResponseMessage> Exec() => request.AddAuth(Token).Post("/api/categories", new { Description = Description });

[Fact]
public async Task ShouldSave_Category_IfInputValid()
{
    await Exec();
    var categoryInDb = context.Categories.FirstOrDefault(c => c.Description == Description);
    categoryInDb.Should().NotBeNull();
}

[Fact]
public async Task ShouldRetuns_Category_IfInputValid()
{
    var res = await Exec();
    var body = await res.BodyAs<KeyValuePairResource>();

    body.Id.Should().BeGreaterThan(0, "Id should by set by EF");
    body.Description.Should().Be(Description, "Is the same description sended on Exec();");
}

[Fact]
public async Task ShouldReturn_BadRequest400_IfDescription_LessThanFourCharacters()
{
    Description = "a";

    var res = await Exec();
    var body = await res.BodyAs<ValidationErrorResource>();

    res.StatusCode.Should().Be(HttpStatusCode.BadRequest);
    body.Errors.Should().ContainKey("Description");
}

[Fact]
public async Task ShouldReturn_BadRequest400_IfDescription_GreaterThanSixtyFourCharacters()
{
    //Create a string of 65 characters long 
    Description = string.Join("a", new char[66]);

    var res = await Exec();
    var body = await res.BodyAs<ValidationErrorResource>();

    res.StatusCode.Should().Be(HttpStatusCode.BadRequest);
    body.Errors.Should().ContainKey("Description");
}

[Fact]
public async Task ShouldReturn_Unauthorized401_IfNoTokenProvided()
{
    Token = "";

    var res = await Exec();

    res.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
}
Enter fullscreen mode Exit fullscreen mode

This test all execution paths of our POST endpoint of Categories! Great job!

Setup and mantain integration tests is hard, I hope I've helped!

Requirements

If you clone the repository you need:

  • .NET Core 2.2 Preview (You can try change the versions on .csproj if you don't want to install the preview)
  • An instance of SQL Server

Inside src/CleanVidly/ Set user secret for SQL Server connection string with:

$ dotnet user-secrets set "ConnectionStrings:vidly_db" "Your connection string"
Enter fullscreen mode Exit fullscreen mode

Then run

$ dotnet ef database update
Enter fullscreen mode Exit fullscreen mode

To run test you have two options, set an environment variable with your test connection string, or hardcode in appSettings.Test.json on the test project, user secrets is just for development environment.

If you want to see the full project go to the the Github Repository

GitHub logo lHersey / VidlyIntegrationTest

A clean implementation of integration test with .NET Core

If you know a better aproach please open an issue! I want to learn about your experience! And if you need help please leave a comment!

Thanks!

Discussion

pic
Editor guide