Header menu logo ApiStub.FSharp

alt text

Easy API Testing ๐Ÿงžโ€โ™€๏ธ

This library makes use of F# computation expressions or CEs to wrap some complexities of WebApplicationFactory<T> when setting up integration tests for a .NET Web app. It comes with a domain specific language (DSL) for "mocking" HttpClient factory in integration tests, and more.

Test .NET C# ๐Ÿค from F#

F# is a great language, but it doesn't have to be scary to try it. Integration and Unit tests are a great way to introduce F# to your team if you are already using .NET or ASPNETCORE.

In fact you can add an .fsproj within a C# aspnetcore solution .sln, and just have a single F# assembly test your C# application from F#, referencing a .csproj file is easy! just use regular dotnet add reference command.

Usage

To use the CE, you must build your CE object first by passing the generic Program (minimal api) or Startup (mvc) type argument to TestWebAppFactoryBuilder<T>.

Sample Use Case

Suppose in your main app (Program or Startup) you call Services.AddHttpClient(or its variants) twice, registering 2 API clients to make calls to other services, say to the outbound routes /externalApi and /anotherApi (let's skip the base address for now). suppose ExternalApiClient invokes an http GET method and the other client makes a POST http call, inside your API client code.


sequenceDiagram Test->>App: GET /Hello App->>Dep1: GET /externalApi Dep1-->>App: Response App->>Dep2: POST /anotherApi Dep2-->>App: Response App-->>Test: Response

HTTP Mocks ๐Ÿคก

It's easy to mock those http clients dependencies (with data stubs) during integration tests making use of ApiStub.FSharp lib, saving quite some code compared to manually implementing the WebApplicationFactory<T> pattern, let's see how below.

F# ๐Ÿฆ” โœจ

open ApiStub.FSharp.CE
open ApiStub.FSharp.BuilderExtensions
open ApiStub.FSharp.HttpResponseHelpers
open Xunit

module Tests =

    // build your aspnetcore integration testing CE
    let test = new TestWebAppFactoryBuilder<Program>()

    [<Fact>]
    let ``Calls Hello and returns OK`` () = task {

        let testApp =
            test { 
                GETJ "/externalApi" {| Ok = "yeah" |}
                POSTJ "/anotherApi" {| Whatever = "yeah" |}
            }

        use client = testApp.GetFactory().CreateClient()

        let! r = client.GetAsync("/Hello")

        r.EnsureSuccessStatusCode()
    } 

C# ๐Ÿค– for ๐Ÿ‘ด๐Ÿฝ๐Ÿฆ–๐Ÿฆ•

if you prefer to use C# for testing, some extension methods are provided to use with C# as well:

GETJ, PUTJ, POSTJ, DELETEJ, PATCHJ

Remember to add this snippet at the end of your Program.cs file for the TestWebAppFactoryBuilder to be able to pick up your configuration:

// Program.cs

// ... all your code, until end of file. 

public partial class Program { }

If you want to access more overloads, you can access the inspect TestWebAppFactoryBuilder<T> members and create your custom extension methods easilly.

using ApiStub.FSharp;
using static ApiStub.Fsharp.CsharpExtensions; 
using Xunit;

public class Tests 
{

    [Fact]
    public async Task CallsHelloAndReturnsOk()
    {
        var webAppFactory = new CE.TestWebAppFactoryBuilder<Web.Sample.Program>()
            .GETJ(Clients.Routes.name, new { Name = "Peter" })
            .GETJ(Clients.Routes.age, new { Age = 100 })
            .GetFactory();

        // factory.CreateClient(); // as needed later in your tests
    }
}

Mechanics ๐Ÿ‘จ๐Ÿฝโ€๐Ÿ”งโš™๏ธ

This library makes use of F# computation expressions to hide some complexity of WebApplicationFactory and provide the user with a domain specific language (DSL) for integration tests in aspnetcore apps.

๐Ÿช†๐Ÿ“ฆ > The main "idea" behind this library is having a CE builder that wraps the creation of a russian doll or chinese boxes of MockHttpHandler to handle mocking requests to http client instances in your application under test or SUT.

The TestsClient CE acts as a reusable and shareable/composable builder CE for WebApplicationFactory...

(new TestWebAppFactoryBuilder<Program>()) // TestWebAppFactoryBuilder<T> is here a WebApplicationFactory (WAF) builder  in essence basically
{
    // --> add stub to builder for WAF
    GETJ "A" {| Response = "OK" |}
    // --> add stub to builder for WAF
    GETJ "B" {| Response = "OK" |}
    // --> add stub to builder for WAF
    GETJ "C" {| Response = "OK" |}

    // each call adds to WAF builder
}
|> _.GetFactory() // until this point the builder can be decorated further / shared / reused in specific test flows

The best way to understand how it all works is checking the code and this member CE method GetFactory() in scope.

If you have ideas for improvements feel free to open an issue/discussion! I do this on my own time, so support is limited but contributions/PRs are welcome ๐Ÿ™

Features ๐Ÿ‘จ๐Ÿปโ€๐Ÿ”ฌ

HTTP Methods ๐Ÿš•

Available HTTP methods in the test dsl to "mock" HTTP client responses are the following:

Basic

    // example of control on request and route value dictionary
    PUT "/externalApi" (fun r rvd -> 
        // read request properties or route, but not content...
        // unless you are willing to wait the task explicitly as result
        {| Success = true |} |> R_JSON 
    )

JSON ๐Ÿ“’

GETJ "/yetAnotherOne" {| Success = true |}

ASYNC Overloads (task) โšก๏ธ

// example of control on request and route value dictionary
    // asynchronously
    POST_ASYNC "/externalApi" (fun r rvd -> 
        task {
            // read request content and meddle here...
            return {| Success = true |} |> R_JSON 
        }
    )

HTTP response helpers ๐Ÿ‘จ๐Ÿฝโ€๐Ÿ”ง

Available HTTP content constructors are:

Configuration helpers ๐Ÿชˆ

BDD (gherkin) Extensions ๐Ÿฅ’

You can use some BDD extension to perform Gherkin-like setups and assertions

they are all async task computations so they can be simply chained together:

// open ...
open ApiStub.FSharp.BDD
open HttpResponseMessageExtensions

module BDDTests =

    let testce = new TestWebAppFactoryBuilder<Startup>()

    [<Fact>]
    let ``when i call /hello i get 'world' back with 200 ok`` () =
            
            let mutable expected = "_"
            let stubData = { Ok = "undefined" }

            // ARRANGE step is divided in CE (arrange client stubs)
            // SETUP: additional factory or service or client configuration
            // and GIVEN the actual arrange for the test 3As.
                
            // setup your test as usual here, test_ce is an instance of TestWebAppFactoryBuilder<TStartup>()
            test_ce {
                POSTJ "/another/anotherApi" {| Test = "NOT_USED_VAL" |}
                GET_ASYNC "/externalApi" (fun r _ -> task { 
                    return { stubData with Ok = expected } |> R_JSON 
                })
            }
            |> SCENARIO "when i call /Hello i get 'world' back with 200 ok"
            |> SETUP (fun s -> task {
            
                let test = s.TestWebAppFactoryBuilder
                
                // any additiona services or factory configuration before this point
                let f = test.GetFactory() 
                
                return {
                    Client = f.CreateClient()
                    Factory = f
                    Scenario = s
                    FeatureStubData = stubData
                }
            }) (fun c -> c) // configure test client here if needed
            |> GIVEN (fun g -> //ArrangeData
                expected <- "world"
                expected |> Task.FromResult
            )
            |> WHEN (fun g -> task { //ACT and AssertData
                let! (r : HttpResponseMessage) = g.Environment.Client.GetAsync("/Hello")
                return! r.Content.ReadFromJsonAsync<Hello>()

            })
            |> THEN (fun w -> // ASSERT
                Assert.Equal(w.Given.ArrangeData, w.AssertData.Ok) 
            )
            |> END

More Examples?

Please take a look at the examples in the test folder for more details on the usage.

How to Contribute โœ๏ธ

Commit linting ๐Ÿ“

This project uses Commitlint npm package and ConventionalCommits specification for commits, so be aware to follow them when committing, via Husky.NET

Versioning ๐Ÿ“š

This repository uses Versionize as a local dotnet tool to version packages when publishing. Versionize relies on conventional commits to work properly.

References ๐Ÿค“

๐Ÿ•Š๏ธ๐ŸŒŽ๐ŸŒณ

JUST_STOP_OIL

Stand With Ukraine

Ceasefire Now

module Tests from index
val test: (obj -> obj)
val task: TaskBuilder
val testApp: obj
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
val client: System.IAsyncDisposable
val r: obj

Type something to start searching.