The compiled code of our .NET Core 8 application is on our GitHub repository. For this test project, which is part our application, we will use MyTested - a well-known library for testing ASP.NET Core MVC. Here, we adapted the library to work with .NET Core 8 and API controllers with Bearer Header Authorization based on JWT token implementation provided by .NET Core. Our .NET Core 8 project is based on BookStore repository and adapted to work with MyTested library.
I found out about MyTested for the first time from BlazorShop repository. At the same time, I found out about Jwt Authentication
implementation from same BlazorShop repository and from aspnetcore-realworld-example repository. Both Jwt Authentication
implementations did not work with original MyTested library, so I decided to find out why. I do not know who engineered MyTested, but I was not able to fully understand how it works. I was able only to add some small pieces of code to make MyTested and my own Jwt Authentication
implementation work and not to break any original MyTested tests. But, what MyTested can do out of the box? The best answer is in MusicStore testing project. For the API controller, here is an example:
#if DEBUG
using BlogAngular.Application.Common.Version;
using BlogAngular.Web.Features;
using MyTested.AspNetCore.Mvc;
using Xunit;
namespace BlogAngular.Test.Routing
{
public class FrontEndRouteTest
{
[Fact]
public void VersionShouldBeRouted()
=> MyMvc
.Pipeline()
.ShouldMap(request => request
.WithMethod(HttpMethod.Get)
.WithLocation("api/v1.0/version"))
.To<VersionController>(c => c.Index())
.Which()
.ShouldReturn()
.ActionResult(result => result.Result(new VersionResponseEnvelope
{
VersionJson = new VersionResponseModel()
}));
}
}
#endif
By basic API controller testing, we mean at least one test per CRUD concept. Here is an example:
Create_tag_should_return_success_with_data
- CreateListing_tags_without_url_parameters_should_return_success_with_all_tags
- ReadEdit_tag_should_return_success_with_data
- UpdateDelete_tag_should_return_success_with_tag_id
- Delete
A particular change we made to MyTested is adding the possibility of testing data validation. In fact, now, we can implement all following tests: BookStore, RealWorld, CleanArchitecture1, and CleanArchitecture2 in a set of beautiful tests. Here are examples of testing data validation using modified version of MyTested library:
Create_tag_with_one_char_should_return_validation_error
- Creates tag name length bellow allowed by database constraintCreate_tag_with_max_chars_should_return_validation_error
- Creates tag name length above allowed by database constraintEdit_tag_with_one_char_should_return_validation_error
- Updates tag name length bellow allowed by database constraintEdit_tag_with_max_chars_should_return_validation_error
- Updates tag name length above allowed by database constraint
Our validation implementation is based mostly on BookStore. One useful technique to validate unique data comes from Conduit and CleanArchitecture. Following are three tests with the constraint that the tag name is unique:
Create_tag_with_same_name_should_fail_with_validation_error
- Creates tag with name when the name has already taken.Edit_tag_with_same_name_should_fail_with_validation_error
- Updates tag name when the name has already taken.Edit_same_tag_with_same_name_should_return_success_with_data
- Updates tag name when the name did not change.
In our application, any MyTested.AspNetCore.Mvc.Exceptions.ValidationErrorsAssertionException
will return 422 with JSON string similar to this:
{
"TagJson.Title": ["The length of 'Tag Json Title' must be 420 characters or fewer. You entered 421 characters."]
}
That represents a standard validation message from FluentValidation
library which can be customized.
In our application, we use Ardalis.GuardClauses.NotFoundException
instead of BaseDomainException. In addition, we use ValidationExceptionHandlerMiddleware to intercept all validation exceptions that return HttpStatusCode.UnprocessableEntity
(422). Unfortunately, MyTested does not work with the middleware concept. But, we can use MyTested.AspNetCore.Mvc.Exceptions.InvocationAssertionException
and FromNotFoundException to test against two common exceptions:
Edit_tag_with_wrong_id_should_fail
- The tag with the specified id does not exist in the database.Update_user_with_malformated_data_should_fail
- The webserver cannot create the object from the json data request.
When it comes to JWT authorization, a big amount of testing consists in testing for invalid JWT tokens:
Update_user_without_authorization_header_should_fail
- tests when JWT token is absentUpdate_user_with_altered_authorization_header_should_fail
- tests when to a valid JWT token is added one characterUpdate_user_with_malformated_authorization_header_should_fail
- tests when JWT token has formata.b
Update_user_with_fake_authorization_header_should_fail
- tests when JWT token has correct formata.b.c
but random charactersUpdate_user_with_incorrect_authorization_header_key_should_fail
- tests when JWT token is valid but was encrypted with a different keyUpdate_user_with_expired_authorization_header_should_fail
- tests when a valid JWT token was expired
These are the most common case scenarios to test against an invalid JWT token and must be done just for one controller!
MyTested cannot catch 401 error code directly. We found a workaround by using HeaderAuthorizationException
The full source code for the .NET Core IdentityService
implementation can be found here
We applied modified version of MyTested library to three popular GitHub repositories: BookStore, RealWorld, and CleanArchitecture. Our quick investigation shows that BookStore can be configurated to work 100% with MyTested while RealWorld works only with anonymous controllers and CleanArchitecture does not work at all. The full test project source code can be found on our GitHub repository.