From a3a90a1bc91b880225d9db07f67aa794859bf7be Mon Sep 17 00:00:00 2001 From: gparmigiani Date: Mon, 12 May 2025 15:34:36 +0200 Subject: [PATCH] feat: renamed TestClient to TestWebAppFactoryBuilder, fix to publish and docs --- .github/workflows/publish.yml | 11 ++- ApiStub.FSharp/BDD.fs | 12 +-- ApiStub.FSharp/CE.fs | 26 +++--- ApiStub.FSharp/Csharp.fs | 12 +-- README.md | 54 ++++++------ docs/index.md | 86 +++++++++++-------- samples/web-csharp/test/Tests.fs | 2 +- .../Web.Sample.Csharp.Test/CSharpTests.cs | 2 +- samples/web/test/Web.Sample.Test/Tests.fs | 2 +- test/BDDTests.fs | 4 +- test/BuilderExtensionsTests.fs | 6 +- test/CETests.fs | 2 +- 12 files changed, 119 insertions(+), 100 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1ecf477..af648a8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,9 +1,13 @@ name: Publish on: - push: - branches: + # trigger only on completion of Build on main + workflow_run: + workflows: ["Build"] + branches: - main + types: + - completed jobs: publish: @@ -29,8 +33,7 @@ jobs: - name: Run Versionize id: versionize - run: | - dotnet versionize + run: dotnet versionize continue-on-error: true - name: No release required diff --git a/ApiStub.FSharp/BDD.fs b/ApiStub.FSharp/BDD.fs index 6fca797..574ba68 100644 --- a/ApiStub.FSharp/BDD.fs +++ b/ApiStub.FSharp/BDD.fs @@ -13,7 +13,7 @@ module BDD = /// Defines a BDD scenario type Scenario<'TStartup when 'TStartup: not struct> = { UseCase: string - TestClient: TestClient<'TStartup> } + TestWAFBuilder: TestWebAppFactoryBuilder<'TStartup> } /// Defines the context propagated through the test type Environment<'TStartup, 'FeatureStubData when 'TStartup: not struct> = @@ -42,9 +42,9 @@ module BDD = /// Scenario builder - let SCENARIO useCase testClient = + let SCENARIO useCase (builder: TestWebAppFactoryBuilder<_>) = { UseCase = useCase - TestClient = testClient } + TestWAFBuilder = builder } |> Step.Scenario /// Setup the Environment for the given scenario @@ -152,17 +152,17 @@ module BDD = // [] sample -// let ``when i call /hello i get 'world' back with 200 ok`` (testClient: TestClient<_>) = +// let ``when i call /hello i get 'world' back with 200 ok`` (TestWebAppFactoryBuilder: TestWebAppFactoryBuilder<_>) = // let stubData = [ 1, 2, 3 ] -// testClient { GET "/hello" (fun _ _ -> $"hello world {stubData}" |> R_TEXT) } +// TestWebAppFactoryBuilder { GET "/hello" (fun _ _ -> $"hello world {stubData}" |> R_TEXT) } // |> SCENARIO "when i call /hello i get 'world' back with 200 ok" // |> SETUP // (fun s -> // task { -// let test = s.TestClient +// let test = s.TestWebAppFactoryBuilder // let f = test.GetFactory() diff --git a/ApiStub.FSharp/CE.fs b/ApiStub.FSharp/CE.fs index 105e41c..4d3f639 100644 --- a/ApiStub.FSharp/CE.fs +++ b/ApiStub.FSharp/CE.fs @@ -9,7 +9,7 @@ open Microsoft.Extensions.Http open Microsoft.AspNetCore.Routing.Template open Microsoft.AspNetCore.Routing -/// computation expression module (builder CE), contains `TestClient` that wraps `WebApplicationFactory` +/// computation expression module (builder CE), contains `TestWebAppFactoryBuilder` (former `TestWebAppFactoryBuilder`) that wraps `WebApplicationFactory` module CE = open BuilderExtensions open HttpResponseHelpers @@ -18,8 +18,8 @@ module CE = let private toAsync stub = fun req args -> task { return stub req args } - /// `TestClient` wraps `WebApplicationFactory` and exposes a builder CE with utility to define api client stubs and other features - type TestClient<'T when 'T: not struct>() = + /// `TestWebAppFactoryBuilder` wraps `WebApplicationFactory` and exposes a builder CE with utility to define api client stubs and other features + type TestWebAppFactoryBuilder<'T when 'T: not struct>() = let factory = new WebApplicationFactory<'T>() let mutable httpMessageHandler: DelegatingHandler = null @@ -82,22 +82,14 @@ module CE = [] member this.Stub - ( - x, - methods, - routeTemplate, - stub: HttpRequestMessage -> RouteValueDictionary -> HttpResponseMessage - ) = + (x, methods, routeTemplate, stub: HttpRequestMessage -> RouteValueDictionary -> HttpResponseMessage) + = this.StubWithOptions(x, methods, routeTemplate, stub |> toAsync, false) [] member this.StubAsync - ( - x, - methods, - routeTemplate, - stub: HttpRequestMessage -> RouteValueDictionary -> HttpResponseMessage Task - ) = + (x, methods, routeTemplate, stub: HttpRequestMessage -> RouteValueDictionary -> HttpResponseMessage Task) + = this.StubWithOptions(x, methods, routeTemplate, stub, false) /// stub operation with stub object (HttpResponseMessage) @@ -232,3 +224,7 @@ module CE = |> web_configure_test_services (fun s -> for custom_config in customConfigureTestServices do custom_config (s) |> ignore) + + /// `TestClient` is a type backfill for `TestWebAppFactoryBuilder`, please switch to the new name if possible + [] + type TestClient<'T when 'T: not struct> = TestWebAppFactoryBuilder<'T> diff --git a/ApiStub.FSharp/Csharp.fs b/ApiStub.FSharp/Csharp.fs index 8f42fbe..bab0b0f 100644 --- a/ApiStub.FSharp/Csharp.fs +++ b/ApiStub.FSharp/Csharp.fs @@ -7,19 +7,21 @@ open ApiStub.FSharp.CE type CsharpExtensions = [] - static member GETJ<'a when 'a: not struct>(x: TestClient<'a>, route: string, stub: obj) = x.GetJson(x, route, stub) + static member GETJ<'a when 'a: not struct>(x: TestWebAppFactoryBuilder<'a>, route: string, stub: obj) = + x.GetJson(x, route, stub) [] - static member POSTJ<'a when 'a: not struct>(x: TestClient<'a>, route: string, stub: obj) = + static member POSTJ<'a when 'a: not struct>(x: TestWebAppFactoryBuilder<'a>, route: string, stub: obj) = x.PostJson(x, route, stub) [] - static member PUTJ<'a when 'a: not struct>(x: TestClient<'a>, route: string, stub: obj) = x.PutJson(x, route, stub) + static member PUTJ<'a when 'a: not struct>(x: TestWebAppFactoryBuilder<'a>, route: string, stub: obj) = + x.PutJson(x, route, stub) [] - static member DELETEJ<'a when 'a: not struct>(x: TestClient<'a>, route: string, stub: obj) = + static member DELETEJ<'a when 'a: not struct>(x: TestWebAppFactoryBuilder<'a>, route: string, stub: obj) = x.DeleteJson(x, route, stub) [] - static member PATCHJ<'a when 'a: not struct>(x: TestClient<'a>, route: string, stub: obj) = + static member PATCHJ<'a when 'a: not struct>(x: TestWebAppFactoryBuilder<'a>, route: string, stub: obj) = x.PatchJson(x, route, stub) diff --git a/README.md b/README.md index 01f737b..8622916 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,14 @@ Access the [documentation website](https://jkone27.github.io/fsharp-integration- ```mermaid sequenceDiagram - participant TestClient as Test + participant TestWebAppFactoryBuilder as Test participant MainApp as App participant DependencyApp as Dep - TestClient->>MainApp: GET /Hello + TestWebAppFactoryBuilder->>MainApp: GET /Hello MainApp->>DependencyApp: GET /externalApi DependencyApp-->>MainApp: Response - MainApp-->>TestClient: Response + MainApp-->>TestWebAppFactoryBuilder: Response ``` @@ -44,45 +44,49 @@ open Xunit module Tests = // build your aspnetcore integration testing CE - let test = new TestClient() + let test = new TestWebAppFactoryBuilder() [] - let ``Calls Hello and returns OK`` () = + let ``Calls Hello and returns OK`` () = task { - task { + let client = + test { + GETJ "/externalApi" {| Ok = "yeah" |} + } + |> _.GetFactory() + |> _.CreateClient() - let client = - test { - GETJ "/externalApi" {| Ok = "yeah" |} - } - |> _.GetFactory() - |> _.CreateClient() + let! r = client.GetAsync("/Hello") - let! r = client.GetAsync("/Hello") + // rest of your tests... - // rest of your tests... - - } + } ``` or in `C#` if you prefer ```csharp using ApiStub.FSharp; +using Xunit; using static ApiStub.Fsharp.CsharpExtensions; -async Task CallsHelloAndReturnsOk () { - - var client = - new CE.TestClient() - .GETJ("/externalApi", new { Ok = "Yeah" }) - .GetFactory() - .CreateClient(); +public class Tests +{ + [Fact] + async Task CallsHelloAndReturnsOk() + { - var r = await client.GetAsync("/Hello"); + var client = + new CE.TestWebAppFactoryBuilder() + .GETJ("/externalApi", new { Ok = "Yeah" }) + .GetFactory() + .CreateClient(); - // rest of your tests... + var r = await client.GetAsync("/Hello"); + // rest of your tests... + } +} ``` ### Test .NET C# ๐Ÿค from F# diff --git a/docs/index.md b/docs/index.md index 470ab94..55acdd4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,7 +14,7 @@ In fact you can add an `.fsproj` within a C# aspnetcore solution `.sln`, and jus ## 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 `TestClient`. +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`. ### Sample Use Case @@ -39,7 +39,7 @@ It's easy to **mock** those http clients dependencies (with data stubs) during i ## F# ๐Ÿฆ” โœจ -* `Program`: to be able to make use of `Program.fs` (e.g. minimal api) as `TestClient()`, make sure to declare an empty `type Program = end class` on top of your Program module containing the `[] main args` method. For older porjects `Startup` can be passed as` TEntryPoint` instead. +* `Program`: to be able to make use of `Program.fs` (e.g. minimal api) as `TestWebAppFactoryBuilder()`, make sure to declare an empty `type Program = end class` on top of your Program module containing the `[] main args` method. For older porjects `Startup` can be passed as` TEntryPoint` instead. ```fsharp @@ -51,25 +51,23 @@ open Xunit module Tests = // build your aspnetcore integration testing CE - let test = new TestClient() + let test = new TestWebAppFactoryBuilder() [] - let ``Calls Hello and returns OK`` () = + let ``Calls Hello and returns OK`` () = task { - task { - - let testApp = - test { - GETJ "/externalApi" {| Ok = "yeah" |} - POSTJ "/anotherApi" {| Whatever = "yeah" |} - } + let testApp = + test { + GETJ "/externalApi" {| Ok = "yeah" |} + POSTJ "/anotherApi" {| Whatever = "yeah" |} + } - use client = testApp.GetFactory().CreateClient() + use client = testApp.GetFactory().CreateClient() - let! r = client.GetAsync("/Hello") + let! r = client.GetAsync("/Hello") - r.EnsureSuccessStatusCode() - } + r.EnsureSuccessStatusCode() + } ``` ## C# ๐Ÿค– for ๐Ÿ‘ด๐Ÿฝ๐Ÿฆ–๐Ÿฆ• @@ -78,7 +76,7 @@ if you prefer to use C# for testing, some extension methods are provided to use `GETJ, PUTJ, POSTJ, DELETEJ, PATCHJ` -Remember to add this snippet at the end of your `Program.cs` file for the `TestClient` to be able to pick up your configuration: +Remember to add this snippet at the end of your `Program.cs` file for the `TestWebAppFactoryBuilder` to be able to pick up your configuration: ```csharp // Program.cs @@ -88,34 +86,52 @@ Remember to add this snippet at the end of your `Program.cs` file for the `TestC public partial class Program { } ``` -If you want to access more overloads, you can access the inspect `TestClient` members and create your custom extension methods easilly. +If you want to access more overloads, you can access the inspect `TestWebAppFactoryBuilder` members and create your custom extension methods easilly. ```csharp using ApiStub.FSharp; using static ApiStub.Fsharp.CsharpExtensions; - -var webAppFactory = new CE.TestClient() - .GETJ(Clients.Routes.name, new { Name = "Peter" }) - .GETJ(Clients.Routes.age, new { Age = 100 }) - .GetFactory(); - -// factory.CreateClient(); // as needed later in your tests - +using Xunit; + +public class Tests +{ + + [Fact] + public async Task CallsHelloAndReturnsOk() + { + var webAppFactory = new CE.TestWebAppFactoryBuilder() + .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](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/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**](https://github.com/jkone27/fsharp-integration-tests/blob/7082d89870bcf353a879c6fcacc74174cea81add/ApiStub.FSharp/CE.fs#L69) or **chinese boxes** of [MockHttpHandler](https://github.com/jkone27/fsharp-integration-tests/blob/7082d89870bcf353a879c6fcacc74174cea81add/ApiStub.FSharp/DelegatingHandlers.fs#L22) to handle mocking requests to http client instances in your application under test or SUT. +๐Ÿช†๐Ÿ“ฆ > The main "idea" behind this library is having a CE builder that wraps the creation of a [**russian doll**](https://github.com/jkone27/fsharp-integration-tests/blob/7082d89870bcf353a879c6fcacc74174cea81add/ApiStub.FSharp/CE.fs#L69) or **chinese boxes** of [MockHttpHandler](https://github.com/jkone27/fsharp-integration-tests/blob/7082d89870bcf353a879c6fcacc74174cea81add/ApiStub.FSharp/DelegatingHandlers.fs#L22) to handle mocking requests to http client instances in your application under test or SUT. -
-graph TD +The TestsClient CE acts as a reusable and shareable/composable builder CE for WebApplicationFactory... - test_ce -->|get| factory -->|get| test_client +```fsharp - test_client -->|HTTP| app -
+(new TestWebAppFactoryBuilder()) // TestWebAppFactoryBuilder 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](https://github.com/jkone27/fsharp-integration-tests/blob/249c3244cd7e20e2168b82a49b6e7e14f2ad1004/ApiStub.FSharp/CE.fs#L176) and this member CE method `GetFactory()` in scope. @@ -191,7 +207,7 @@ You can use some BDD extension to perform [Gherkin-like setups and assertions](h they are all async `task` computations so they can be simply chained together: -* `SCENARIO`: takes a `TestClient` as input and needs a name for your test scenario +* `SCENARIO`: takes a `TestWebAppFactoryBuilder` 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 @@ -205,7 +221,7 @@ open HttpResponseMessageExtensions module BDDTests = - let testce = new TestClient() + let testce = new TestWebAppFactoryBuilder() [] let ``when i call /hello i get 'world' back with 200 ok`` () = @@ -217,7 +233,7 @@ module BDDTests = // 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() + // setup your test as usual here, test_ce is an instance of TestWebAppFactoryBuilder() test_ce { POSTJ "/another/anotherApi" {| Test = "NOT_USED_VAL" |} GET_ASYNC "/externalApi" (fun r _ -> task { @@ -227,7 +243,7 @@ module BDDTests = |> SCENARIO "when i call /Hello i get 'world' back with 200 ok" |> SETUP (fun s -> task { - let test = s.TestClient + let test = s.TestWebAppFactoryBuilder // any additiona services or factory configuration before this point let f = test.GetFactory() diff --git a/samples/web-csharp/test/Tests.fs b/samples/web-csharp/test/Tests.fs index 50654f9..48d650b 100644 --- a/samples/web-csharp/test/Tests.fs +++ b/samples/web-csharp/test/Tests.fs @@ -6,7 +6,7 @@ open ApiStub.FSharp open ApiStub.FSharp.CE open System.Net.Http.Json -let ce = (new TestClient()) { +let ce = (new TestWebAppFactoryBuilder()) { GETJ "persons" [{| Name = "John" ; Age = 30 |}] } diff --git a/samples/web/test/Web.Sample.Csharp.Test/CSharpTests.cs b/samples/web/test/Web.Sample.Csharp.Test/CSharpTests.cs index 41ccc84..96a60fb 100644 --- a/samples/web/test/Web.Sample.Csharp.Test/CSharpTests.cs +++ b/samples/web/test/Web.Sample.Csharp.Test/CSharpTests.cs @@ -13,7 +13,7 @@ public class CSharpTests { private static WebApplicationFactory getWebAppFactory() => // create an instance of the test client builder - new TestClient() + new TestWebAppFactoryBuilder() .GETJ(Clients.Routes.name, new { Name = "Peter" }) .GETJ(Clients.Routes.age, new { Age = 100 }) .GetFactory(); diff --git a/samples/web/test/Web.Sample.Test/Tests.fs b/samples/web/test/Web.Sample.Test/Tests.fs index af3d644..86b9039 100644 --- a/samples/web/test/Web.Sample.Test/Tests.fs +++ b/samples/web/test/Web.Sample.Test/Tests.fs @@ -10,7 +10,7 @@ open System.Net.Http.Json let webAppFactory = - new TestClient() + new TestWebAppFactoryBuilder() |> fun x -> x { GETJ Web.Sample.Clients.Routes.name {| Name = "Peter" |} GETJ Web.Sample.Clients.Routes.age {| Age = 100 |} diff --git a/test/BDDTests.fs b/test/BDDTests.fs index 94d415a..5a84473 100644 --- a/test/BDDTests.fs +++ b/test/BDDTests.fs @@ -30,7 +30,7 @@ open Xunit.Abstractions module BDDTests = - let testce = new TestClient() + let testce = new TestWebAppFactoryBuilder() [] @@ -48,7 +48,7 @@ module BDDTests = (fun s -> task { - let test = s.TestClient + let test = s.TestWAFBuilder let f = test.GetFactory() diff --git a/test/BuilderExtensionsTests.fs b/test/BuilderExtensionsTests.fs index cc07693..d2876ca 100644 --- a/test/BuilderExtensionsTests.fs +++ b/test/BuilderExtensionsTests.fs @@ -27,9 +27,7 @@ open ApiStub.FSharp.BDD open HttpResponseMessageExtensions open Xunit.Abstractions -type ISomeSingleton = - interface - end +type ISomeSingleton = interface end type SomeSingleton(name: string) = class @@ -38,7 +36,7 @@ type SomeSingleton(name: string) = type BuilderExtensionsTests(testOutput: ITestOutputHelper) = - let testce = new CE.TestClient() + let testce = new CE.TestWebAppFactoryBuilder() interface IDisposable with member this.Dispose() = (testce :> IDisposable).Dispose() diff --git a/test/CETests.fs b/test/CETests.fs index fce0544..896803e 100644 --- a/test/CETests.fs +++ b/test/CETests.fs @@ -28,7 +28,7 @@ type MyOpenapi = OpenApiClientProvider<"swagger.json"> type CETests() = - let testce = new CE.TestClient() + let testce = new CE.TestWebAppFactoryBuilder() interface IDisposable with member this.Dispose() = (testce :> IDisposable).Dispose()