Testing ASP.NET APIs with Expecto

It has been a while since I last wrote down something here. However, I believe this to be one of the most useful "tricks" I have learned the past few weeks.

Running "end to end integration tests" on endpoints in F# have always been a struggle for me. While running tests, I want to keep the application both stateless and running the whole test suite concurrently.

Fortunately, Expecto provides an easy way to add fixtures to your tests. This means that we can leverage this feature to start a new web server for each test and guarantee that we always have a clean state at the beginning of a test.

Before starting it, let's assume some things on our code:

  • There's a createHost function that receives a port number, a Host builder and returns a "built builder".
  • We just need an HttpClient pointing to our web api on our tests.
let withApi test = task {
  let builder = Host.CreateDefaultBuilder()

  // passing a ~zero~ port to our app makes ASP.NET assign a random
  // port when initializing the service, this avoids conflicts while
  // running tests in parallel.
  let hostPort = 0
  let host = createHost hostPort builder

  let server = host.Services.GetRequiredService<IServer>()
  let addressFeature = server.Features.Get<IServerAddressesFeature>()

  do! host.StartAsync()

  let port =
    let appUri = addressFeature.Addresses |> Seq.head |> Uri
    appUri.Port

  let client = new HttpClient()
  client.BaseAddress <- Uri(sprintf "http://localhost:%d" port)

  do! test client
  do! host.StopAsync()
}

Consuming this fixture is rather easy:

testList "api tests" [
  yield! testFixtureTask withApi [
    "get user", fun client -> task {
      let! user = client.GetAsync("/user/admin")
      // ...
    }
  ]
]