~glorifiedgluer/gluer.org

14fee9a589f5257dca7bd91e259a236fa660de16 — Victor Freire 4 months ago ce670df
article: testing-asp-dot-net-apis-with-expecto
1 files changed, 71 insertions(+), 0 deletions(-)

M content-org/content.org
M content-org/content.org => content-org/content.org +71 -0
@@ 5233,6 5233,77 @@ let factory = serviceProvider.GetService<IHttpClientFactory>();
let client = factory.CreateClient()
#+end_src

** Testing ASP.NET APIs with Expecto                         :dotnet:fsharp:
:PROPERTIES:
:EXPORT_DATE: 2023-07-10
:EXPORT_FILE_NAME: testing-asp-dot-net-apis-with-expecto
:EXPORT_HUGO_SLUG: testing-asp-dot-net-apis-with-expecto
:END:

#+begin_description
On how to concurrently test ASP.NET APIs with Expecto.
#+end_description

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, [[https://github.com/haf/expecto][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 [[https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.host.createdefaultbuilder?view=dotnet-plat-ext-7.0][Host
  builder]] and returns a "built builder".
- We just need an [[https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.httpclient?view=net-7.0][HttpClient]] pointing to our web api on our tests.

#+begin_src fsharp
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()
}
#+end_src

Consuming this fixture is rather easy:

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

* Footnotes

[fn:45] Fun fact: I introduced the dotnet module on devenv!