Skip to content

Commit 681db3a

Browse files
committed
test literate programming scripts
1 parent 0a0b361 commit 681db3a

File tree

10 files changed

+576
-0
lines changed

10 files changed

+576
-0
lines changed

.devcontainer/devcontainer.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"tasks": {
3+
"test": "dotnet tool restore && dotnet fsdocs build",
4+
"build": "dotnet tool restore && dotnet build",
5+
"launch": "dotnet tool restore && dotnet build && dotnet fsdocs watch"
6+
}
7+
}

docs/bdd_extensions.fsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
(*** hide ***)
2+
#r "nuget: FSharp.Formatting, 11.3.0"
3+
4+
(**
5+
## BDD (gherkin) Extensions 🥒
6+
7+
You can use some BDD extension to perform [Gherkin-like setups and assertions](https://cucumber.io/docs/gherkin/reference/)
8+
9+
they are all async `task` computations so they can be simply chained together:
10+
11+
* `SCENARIO`: takes a `TestClient<TStartup>` as input and needs a name for your test scenario
12+
* `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
13+
* `GIVEN`: takes a "test environment" or another "given" and returns a "given" step
14+
* `WHEN`: takes a "given" or another "when" step, and returns a a "when" step
15+
* `THEN`: takes a "when" step and asserts on it, returns the same "when" step as result to continue asserts
16+
* `END`: disposes the "test environment" and concludes the task computation
17+
18+
```fsharp
19+
// open ...
20+
open ApiStub.FSharp.BDD
21+
open HttpResponseMessageExtensions
22+
23+
module BDDTests =
24+
25+
let testce = new TestClient<Startup>()
26+
27+
[<Fact>]
28+
let ``when i call /hello i get 'world' back with 200 ok`` () =
29+
30+
let mutable expected = "_"
31+
let stubData = { Ok = "undefined" }
32+
33+
// ARRANGE step is divided in CE (arrange client stubs)
34+
// SETUP: additional factory or service or client configuration
35+
// and GIVEN the actual arrange for the test 3As.
36+
37+
// setup your test as usual here, test_ce is an instance of TestClient<TStartup>()
38+
test_ce {
39+
POSTJ "/another/anotherApi" {| Test = "NOT_USED_VAL" |}
40+
GET_ASYNC "/externalApi" (fun r _ -> task {
41+
return { stubData with Ok = expected } |> R_JSON
42+
})
43+
}
44+
|> SCENARIO "when i call /Hello i get 'world' back with 200 ok"
45+
|> SETUP (fun s -> task {
46+
47+
let test = s.TestClient
48+
49+
// any additiona services or factory configuration before this point
50+
let f = test.GetFactory()
51+
52+
return {
53+
Client = f.CreateClient()
54+
Factory = f
55+
Scenario = s
56+
FeatureStubData = stubData
57+
}
58+
}) (fun c -> c) // configure test client here if needed
59+
|> GIVEN (fun g -> //ArrangeData
60+
expected <- "world"
61+
expected |> Task.FromResult
62+
)
63+
|> WHEN (fun g -> task { //ACT and AssertData
64+
let! (r : HttpResponseMessage) = g.Environment.Client.GetAsync("/Hello")
65+
return! r.Content.ReadFromJsonAsync<Hello>()
66+
67+
})
68+
|> THEN (fun w -> // ASSERT
69+
Assert.Equal(w.Given.ArrangeData, w.AssertData.Ok)
70+
)
71+
|> END
72+
73+
```

docs/configuration_helpers.fsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
(*** hide ***)
2+
#r "nuget: FSharp.Formatting, 11.3.0"
3+
4+
(**
5+
## Configuration helpers 🪈
6+
7+
* `WITH_SERVICES`: to override your ConfigureServices for tests
8+
* `WITH_TEST_SERVICES`: to override your specific test services (a bit redundant in some cases, depending on the need)
9+
10+
### Example
11+
12+
Here is an example of how to use the configuration helpers:
13+
14+
```fsharp
15+
open ApiStub.FSharp.CE
16+
open ApiStub.FSharp.BuilderExtensions
17+
open ApiStub.FSharp.HttpResponseHelpers
18+
open Xunit
19+
20+
type ISomeSingleton =
21+
interface
22+
end
23+
24+
type SomeSingleton(name: string) =
25+
class
26+
interface ISomeSingleton
27+
end
28+
29+
module Tests =
30+
31+
let test = new TestClient<Startup>()
32+
33+
[<Fact>]
34+
let ``WITH_SERVICES registers correctly`` () =
35+
task {
36+
37+
let testApp =
38+
test {
39+
GETJ "hello" {| ResponseCode = 1001 |}
40+
41+
WITH_SERVICES(fun (s: IServiceCollection) ->
42+
s.AddSingleton<ISomeSingleton>(new SomeSingleton("John")))
43+
}
44+
45+
let fac = testApp.GetFactory()
46+
47+
let singleton = fac.Services.GetRequiredService<ISomeSingleton>()
48+
49+
Assert.NotNull(singleton)
50+
51+
let client = fac.Services.GetRequiredService<HttpClient>()
52+
53+
let! resp = client.GetFromJsonAsync<{| ResponseCode: int |}>("hello")
54+
55+
Assert.Equal(1001, resp.ResponseCode)
56+
}
57+
58+
[<Fact>]
59+
let ``WITH_TEST_SERVICES registers correctly`` () =
60+
task {
61+
62+
let singletonMock = { new ISomeSingleton }
63+
64+
let testApp =
65+
test {
66+
GETJ "hello" {| ResponseCode = 1001 |}
67+
68+
WITH_SERVICES(fun (s: IServiceCollection) ->
69+
s.AddSingleton<ISomeSingleton>(new SomeSingleton("John")))
70+
71+
WITH_TEST_SERVICES(fun s -> s.AddSingleton(singletonMock))
72+
}
73+
74+
let fac = testApp.GetFactory()
75+
76+
let singleton = fac.Services.GetRequiredService<ISomeSingleton>()
77+
78+
Assert.NotNull(singleton)
79+
Assert.Same(singletonMock, singleton)
80+
81+
let client = fac.Services.GetRequiredService<HttpClient>()
82+
83+
let! resp = client.GetFromJsonAsync<{| ResponseCode: int |}>("hello")
84+
85+
Assert.Equal(1001, resp.ResponseCode)
86+
}
87+
```

docs/features.fsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
(*** hide ***)
2+
#r "nuget: FSharp.Formatting, 11.3.0"
3+
4+
(**
5+
## Features 👨🏻‍🔬
6+
7+
* **HTTP client mock DSL**:
8+
* supports main HTTP verbs
9+
* support for JSON payload for automatic object serialization
10+
* **BDD spec dsl extension** (behaviour driven development)
11+
* to express tests in gherkin GIVEN, WHEN, THEN format if you want to
12+
* EXTRAS
13+
* utilities for test setup and more...
14+
15+
### Example
16+
17+
Here is an example of how to use the features of this library:
18+
19+
```fsharp
20+
open ApiStub.FSharp.CE
21+
open ApiStub.FSharp.BuilderExtensions
22+
open ApiStub.FSharp.HttpResponseHelpers
23+
open Xunit
24+
25+
module Tests =
26+
27+
// build your aspnetcore integration testing CE
28+
let test = new TestClient<Startup>()
29+
30+
[<Fact>]
31+
let ``Calls Hello and returns OK`` () =
32+
33+
task {
34+
35+
let testApp =
36+
test {
37+
GETJ "/externalApi" {| Ok = "yeah" |}
38+
POSTJ "/anotherApi" {| Whatever = "yeah" |}
39+
}
40+
41+
use client = testApp.GetFactory().CreateClient()
42+
43+
let! r = client.GetAsync("/Hello")
44+
45+
r.EnsureSuccessStatusCode()
46+
}
47+
```

docs/http_methods.fsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
(*** hide ***)
2+
#r "nuget: FSharp.Formatting, 11.3.0"
3+
4+
(**
5+
## HTTP Methods 🚕
6+
7+
Available HTTP methods in the test DSL to "mock" HTTP client responses are the following:
8+
9+
### Basic
10+
11+
* `GET`, `PUT`, `POST`, `DELETE` - for accessing request, route parameters and sending back HttpResponseMessage (e.g. using R_JSON or other constructors)
12+
13+
```fsharp
14+
// example of control on request and route value dictionary
15+
PUT "/externalApi" (fun r rvd ->
16+
// read request properties or route, but not content...
17+
// unless you are willing to wait the task explicitly as result
18+
{| Success = true |} |> R_JSON
19+
)
20+
```
21+
22+
### JSON 📒
23+
24+
* `GETJ`, `PUTJ`, `POSTJ`, `DELETEJ` - for objects converted to JSON content
25+
26+
```fsharp
27+
GETJ "/yetAnotherOne" {| Success = true |}
28+
```
29+
30+
### ASYNC Overloads (task) ⚡️
31+
32+
* `GET_ASYNC`, `PUT_ASYNC`, `POST_ASYNC`, `DELETE_ASYNC` - for handling asynchronous requests inside a task computation expression (async/await) and mock dynamically
33+
34+
```fsharp
35+
// example of control on request and route value dictionary
36+
// asynchronously
37+
POST_ASYNC "/externalApi" (fun r rvd ->
38+
task {
39+
// read request content and meddle here...
40+
return {| Success = true |} |> R_JSON
41+
}
42+
)
43+
```
44+
45+
### HTTP response helpers 👨🏽‍🔧
46+
47+
Available HTTP content constructors are:
48+
49+
* `R_TEXT`: returns plain text
50+
* `R_JSON`: returns JSON
51+
* `R_ERROR`: returns an HTTP error

docs/index.md renamed to docs/index.fsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
(*** hide ***)
2+
#r "nuget: FSharp.Formatting, 11.3.0"
3+
4+
(**
15
# ApiStub.FSharp
26
37
<img width="331" alt="Screenshot 2024-12-16 at 13 01 33" src="https://github.com/user-attachments/assets/5095d6cb-63bb-4644-836f-99b5355870fe" />

docs/mechanics.fsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
(*** hide ***)
2+
#r "nuget: FSharp.Formatting, 11.3.0"
3+
4+
(**
5+
## Mechanics
6+
7+
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.
8+
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.
9+
10+
If you have ideas for improvements feel free to open an issue/discussion!
11+
I do this on my own time, so support is limited but contributions/PRs are welcome 🙏
12+
13+
### Example
14+
15+
Here is an example of how to use F# computation expressions with this library:
16+
17+
```fsharp
18+
open ApiStub.FSharp.CE
19+
open ApiStub.FSharp.BuilderExtensions
20+
open ApiStub.FSharp.HttpResponseHelpers
21+
open Xunit
22+
23+
module Tests =
24+
25+
// build your aspnetcore integration testing CE
26+
let test = new TestClient<Startup>()
27+
28+
[<Fact>]
29+
let ``Calls Hello and returns OK`` () =
30+
31+
task {
32+
33+
let testApp =
34+
test {
35+
GETJ "/externalApi" {| Ok = "yeah" |}
36+
POSTJ "/anotherApi" {| Whatever = "yeah" |}
37+
}
38+
39+
use client = testApp.GetFactory().CreateClient()
40+
41+
let! r = client.GetAsync("/Hello")
42+
43+
r.EnsureSuccessStatusCode()
44+
}
45+
```

0 commit comments

Comments
 (0)