Testing F# web service

Giraffe is a powerful framework that helps us to build web application and services using F# and .NET Core. The following article shows how to test a giraffe web service.

Since last few months I started to focus on F# world, I have write about web development through F# in following articles:  Web development in F#: getting started and Build web service using F# and ASP.NET Core;

This article takes the following repo https://github.com/samueleresca/Blog.FSharpOnWeb as case of study.

Testing concepts from OO

When we think about unit testing, some keywords in on our minds are: dependency injection, IoC and mock.

Speaking about FP, it is totally counterproductive try to apply OO concepts to functional approach, therefore the functional approach requires a way to support unit testing, in OOP it is common to isolate I/O-related operations into dependencies that can be mocked easily, thus FP allows to handle dependencies through concepts like: parameters injection and composition.

For example, let’s take the following implementation:

As you can see, all the functions take as input the LabelsContext type. The LabelsContext is high coupled with our data source. Since we want to make our code testable, we must expose the context as input of our function.

Testing a  Giraffe web service

Since Giraffe is based on ASP.NET Core, most of the testing tools of ASP.NET Core can be transferred in Giraffe testing. Before start, I recommend you to check the implementation described in this article: Build web service using F# and ASP.NET Core.

Following example shows how to test a Giraffe web service at different levels:

  • Repository level: we will use the Microsoft.EntityFrameworkCore.InMemory package in order to test our repository emulating the underlying data source;
  • Handler level: we will mock the HttpContext in order to call our handlers and check the resulting response, also in that case we will use the Microsoft.EntityFrameworkCore.InMemory to emulate our data source;
  • Integration level: in order to perform some integration tests we’ll setup the Microsoft.AspNetCore.TestHost package and call through an HttpClient the APIs exposed by our web service;

Testing repository level

Let’s start by testing the repository level of our service. As said before the Repository level is implemented in that way:

We can use the injected HttpContext in order to test our Repository. Through the use of the  Microsoft.EntityFrameworkCore.InMemory package we can inject an in-memory context:

Testing handler level

The handler layer aim is to handle incoming http requests and serve correct resources. All the handlers are implemented in the Handlers.fs file.

Each handler is mapped on a specific route, each route may accept incoming data and after that, handlers trigger the request validation and they pass data through ours repository functions. The following snippet shows the route mapping of our application:

Since we want to test our handlers, we should proceed as follows:

  1.  using initializeInMemoryContext,populateContext in order to store our test data in memory;
  2. setup some utility functions like: getBody, getContentType, configureContext. They are useful in order to execute our tests;
  3. execute tests of our suite;

Let’s take a look to our HandlersTests.fs file:

Since HttpContext is the base building block of our tests, each of them prepares the request inside the context and pass it to our handlers. In order to obtain the same routing map of our application, each test will reference App.webApp routing. Let put the focus on a single test case:

First step is to define our in-memory context. Secondly,  we serialize our test data and configure our context. Finally, we put all the values inside our request.

Once the arrange phase is finished we proceed by stressing and acting over our function under test, and then we proceed with the arrange phase, by checking the response.

Test host testing

Another approach is to perform tests by using the Microsoft.AspNetCore.TestHost package. The main aim of the TestHost package is to serve test requests without the need for a real web host.

Since we are testing our service in an almost real infrastructure we must prepare our service to deal with a Testing environment. Thus we update our Program.fs file as follow:

configure and configureServices change their behaviour according to the current environment: in that case, the configureServices function uses InMemory provider when the current environment is Test, otherwise it use the default provider.

This is an implementation example of the test host approach:

Since we are performing a TestHost test, each test generates a new TestServer using createHost method, and create a new client. The client will call the corresponding route and it will make some assertions on the HttpResponseMessage content.

Considerations

You have seen how to tests some Giraffe APIs. We have tested some CRUD operations on database by using Microsoft.EntityFrameworkCore.InMemory package which simulates a real database.

By the way, the InMemory packages is not always a valid solution, indeed we may encounter different problems:

  • Your tests are not isolated until they use the same databaseName;
  • The InMemory package doesn’t exactly reflect the constraint of your real database;

If you have a codebase which implements more advanced logics than some CRUD operations the aim should be to keep functions as pure as possibile. Furthermore,  the best way to test and describe logics is by using an unit test approach.

You can find all code inside the following repository. In the test project you also find some fixture files:

  • Fixture.fs which contains some utilities function;
  • HttpFunc.fs implements some functions to deal with HttpClient type;

Final thoughts

That article describes a possible testing approach on Giraffe framework. I suggest you to take a look @ Build web service using F# and ASP.NET Core since it is strictly related to it.

I also suggest you to watch the following talk about Functional design patterns in F# in order to make your code more testable and maintainable.

Cheers 🙂

Samuele

Cover image by Peter Iliev