Automated Tests

Automated Tests

The template contains a unit test project to run automated tests.
With some examples and utility methods.

I run automated tests by calling the
real http endpoint with an in memory version of the API and a
real instance of postgres spun up by
TestContainers in Docker.

Libraries and Tools

Automated Test Example

AspNetCore.Mvc.Testing gives you something called a WebApplicationFactory for testing.
It spins up an in memory version of your entire WebApi.
You can naturally also override parts of configuration and service registration for testing purposes.

You can check out how I did that in the ApiWithDb class.
All I'm doing there is wiring up the WebAppFactory with TestContainers and xUnit's LifeTime as a Fixture.

Sticking with the theme of thinking in endpoints.
I want all my tests to essentially look like this:

var response = await apiClient.Post<Request, Response>("api/do-something", new Request { SomeParameter = 5});

Notice how I did not call any internal class. No Service, no Handler, no Calculator.Double().

I am going through the front door. I am testing the actual contract we are exposing to the outside world.

I am making an "http" request to a "server" at a route and im getting back an "http" response.
(Of course no http calls are being made here. AspNetCore is doing all of this in memory)

This is the ONLY real way our API actually ever gets used. So why would I test anything else?

Here is a real example.
In theory I would like to only interact with actual API endpoints in my tests. In practice this is just not very achievable.

This test is spinning up an empty postgres database in docker.
Then when we start up our API it seeds standard data. Like Admin users and roles.

public async Task DeliveryStatusChangesToShippedToCustomer()
    // --Arrange--
    // Create a simple delivery in the db. Not the way we do in reality.
    var delivery = new Delivery { Status = DeliveryStatus.OnTheRoad };
    using (var createContext = contextFactory.CreateDbContext())
        await createContext.SaveChangesAsync();
    // --Act--
    // Yes, we need to log in. Our real endpoint is secure and often requires data about the current user.
    await LoginAsAdmin();
    // Move the delivery into the ShippedToCustomer status the way we actually do in reality.
    await apiClient.PostAsJsonAsync("api/examples/delivery-ship-to-customer", 
        new DeliveryShipToCustomerRequest { DeliveryId = delivery.Id });
    // Get the delivery the way we actually do in reality. 
    var deliveryResponse = await apiClient.Get<GetDeliveryRequest, GetDeliveryResponse>(
        "api/examples/get-delivery", new GetDeliveryRequest { DeliveryId = delivery.Id });
    // --Assert--

This is the most commen pattern I found myself using.

The Arrange / Assert parts often are not going through the front door.
Sometimes it does, often times you have to get the system in a specific state and it becomes unpractical.
Or you want to assert a side effect on some internal table that never gets accessed publicly.

The Act part is almost always going through the front door.

Final Thoughts

That is the way I like to conceptualize automated tests. I don't even use the word unit test or integration test.
The difference between a Unit and an Integration test using TestContainers and Docker becomes pretty blurry.

I found that even during initial development, this is a nice workflow of writing and debugging code.
Even quicker than using Postman or the build in Swagger / OpenAPI page.

Again, nothing in this code base stops you from going crazy on Fakes and Stubs and test the IService you call in your endpoint directly.

FastEndpoints also has documentation (opens in a new tab) on how they would do testing.


TestContainers can probably do way more impressive things. Spinning up and tearing down any image, can do a lot for these automated tests. Way more than just the database.

This is where my "no abstractions" style can get dangerous.

  • If we use Azure's IBlobStorageClient or an IS3Client in development environment and dont do any overrides, every test absolutely WILL create a file and store it in the cloud.
  • If we simply make any http call to an external API in an endpoint. For example OpenAI.
  • Sendgrid could send out some emails.

These are all very solvable problems, but they are more advanced.
You can probably work around it by

  • Knowing what your endpoint actually does.
  • Registering a fake IS3Client in the test fixture.
  • Stub the HttpClientFactory.
  • Honestly TestContainers can probably do some magic, if you actually want to verify that a certain file gets created.
  • Maybe you actually DO need an abstraction now. They are not illegal.

Key Takaways

in a way this is more complex than more normal testing, but also easier.

People actually write more tests and the tests they write are more useful and true to reality. More tests get written because you dont have to fiddle with stubs, fakes, bogus data etc. You dont have to constantly update dependencies.

Its super simple and super stable to send a reuqest and get back a response. Setting up is also way easier, since it uses the most standard tool: EF Core.