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
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"]))
};
}
Use them inside ConfigureServices method with:
services
.AddAuthentication(GetAuthenticationOptions)
.AddJwtBearer(GetJwtBearerOptions);
And add this in the Configure method
app.UseAuthentication();
app.UseMvc();
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);
}
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();
}
}
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>());
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();
}
}
And in OnModelCreating call this to read all the configuration classes on the current assembly:
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
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);
}
}
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;
}
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
}
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;
}
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
*/
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);
}
I just call
res.BodyAs<Employees[]>();
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
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;
}
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();
}
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();
}
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();
}
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();
}
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!)
}
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);
}
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"
Then run
$ dotnet ef database update
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
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!
Top comments (0)