fsharp-integration-tests icon indicating copy to clipboard operation
fsharp-integration-tests copied to clipboard

Ceasefire Now Stand With Ukraine JUST_STOP_OIL

ApiStub.FSharp NuGet Badge

You have an ASP NET NET6+ (NET6 is LTS in 2023) dotnet API, and you want to simplify HTTP stubs for integration testing, so you can make use of these Computation Expressions (CE) to simplify your tests with some integration testing HTTP stubs DSL.

To use the CE, you must build your CE object first by passing the generic Startup type argument. For how the library is implemented, it still needs you to provide a Startup class, future versions might make use of Program only from minimal API (this already uses WebApplication only anyway).

USAGE

Suppose your server registers 2 API clients internally to make calls to other services, say to the outbound routes externalApi and anotherApi, one client using GET and another using POST methods inside your API client code.

It's easy to mock those endpoints (with data stubs) during integration tests in this way (or similar)...

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 TestClient<Startup>()

    [<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()
        } 

HTTP MOCKING METHODS

Available HTTP mocking methods in the test dsl are:

  • GET, PUT, POST, DELETE - for accessing request, route parameters and sending back HttpResponseMessage (e.g. using R_JSON or other constructors)
    // 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 
    )
  • GETJ, PUTJ, POSTJ, DELETEJ - for objects converted to JSON content
GETJ "/yetAnotherOne" {| Success = true |}
  • GET_ASYNC, PUT_ASYNC, POST_ASYNC, DELETE_ASYNC - for handling asynchronous requests inside a task computation expression (async/await) and mock dynamically
// 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 CONSTRUCTORS

Available HTTP content constructors are:

  • R_TEXT: returns plain text
  • R_JSON: returns JSON
  • R_ERROR: returns an HTTP error

Configure Services HELPERS

  • WITH_SERVICES: to override your ConfigureServices for tests
  • WITH_TEST_SERVICES: to override your specific test services (a bit redundant in some cases, depending on the need)

BDD 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:

  • SCENARIO: takes a TestClient<TStartup> as input and needs a name for your test scenario
  • SETUP: takes a scenario as input and can be used to configure the "test environmenttest": factory and the API client, additionally takes a separate API client configuration
  • GIVEN: takes a "test environment" or another "given" and returns a "given" step
  • WHEN: takes a "given" or another "when" step, and returns a a "when" step
  • THEN: takes a "when" step and asserts on it, returns the same "when" step as result to continue asserts
  • END: disposes the "test environment" and concludes the task computation
// open ...
open ApiStub.FSharp.BDD
open HttpResponseMessageExtensions

module BDDTests =

    let testce = new TestClient<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 TestClient<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.TestClient
                
                // 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.

ApiStub.FSharp.Stubbery

A version using the Stubbery library is also present for "compatibility" when migrating from stubbery versions of pre existing integration tests, in your integration tests setup.

In general, it's advised to not have dependencies or run any in-memory HTTP server if possible, so the minimal version is preferred.

open ApiStub.FSharp.Stubbery.StubberyCE
open ApiStub.FSharp.BuilderExtensions
open ApiStub.FSharp.HttpResponseHelpers
open Xunit

module Tests =

    // build your aspnetcore integration testing CE using Stubbery library
    // for serving HTTP stubs
    let test_stubbery = new TestStubberyClient<Startup>()

    [<Fact>]
    let ``Integration test with stubbery`` () =

        task {

            let testApp =
                test_stubbery { 
                    GET "/externalApi" (fun r args -> expected |> box)
                }

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

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

            r.EnsureSuccessStatusCode()
        } 

How to Contribute

  • Search for an open issue or report one, and check if a similar issue was reported first
  • feel free to get in touch, to fork and check out the repo
  • test and find use cases for this library, testing in F# is awesome!!!!

References