Repository F# setup
open System
open System.IO
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Layers
open Axial.Console
open Axial.FileSystem
open Axial.Hosting
open Axial.Hosting.Browser
open Axial.Hosting.Node
open Axial.HttpClient
open Axial.PlatformService
open Axial.Process
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScript

Testing And Layers

This page shows how the single IHttp.Send boundary makes HTTP workflows testable without a mocking library.

A Complete Fake In A Few Lines

The whole service surface is one method, and Response.create builds synthetic transcripts from an explicit timestamp:

type TestEnv =
    { Http: IHttp }
    interface IHasHttp with
        member this.Http = this.Http

let stub status body =
    let startedAt = DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)
    { Http =
        { new IHttp with
            member _.Send(_, _) = async { return Ok(Response.create startedAt status body) } } }

[<Fact>]
let ``decodes the user payload`` () =
    let env = stub 200 """{"id":1,"name":"Ada"}"""
    let result = Http.getJson decodeUser "https://api.example.test/users/1" |> Flow.runSync env
    test <@ result = Exit.Success { Id = 1; Name = "Ada" } @>
Because the fake receives the full `HttpRequest`, tests can also assert on what was sent — method, URL, query, headers, and body are all plain data. Returning `Error(HttpError.TimedOut(...))` from a fake exercises retry and fallback paths deterministically, with no network and no clock.

The Live Service

Http.live adapts an explicit IClock and one HttpClient; Layer.succeed (Http.live …) exposes them as a layer:

type AppEnv =
    { Http: IHttp }
    interface IHasHttp with
        member this.Http = this.Http

let appLayer (clock: IClock) (client: HttpClient) : Layer<unit, Never, AppEnv> =
    layer {
        let! http = Layer.succeed (Http.live clock client)
        return { Http = http }
    }

workflow
|> Layer.provide (appLayer Clock.live client)
|> Flow.runSync ()
Reuse one `HttpClient` per application, exactly as .NET recommends: connection pooling, DNS rotation handlers, and proxy settings stay standard `HttpClient` concerns. Axial adds the typed request/response boundary on top without hiding the client or the clock used for transcript timestamps and durations. Tests can pass `Clock.fromValue` or another `IClock` fake for deterministic time.

Base addresses configured on the client work as usual — relative request URLs resolve against client.BaseAddress:

let client = new HttpClient(BaseAddress = Uri "https://api.example.com/")
// Http.get "users/1" now resolves to https://api.example.com/users/1
## Composing With Other Services

Service records compose the same way as the other platform packages:

type WorkerEnv =
    { HttpService: IHttp
      ProcessService: IProcess }
    interface IHasHttp with member this.Http = this.HttpService
    interface IHasProcess with member this.Process = this.ProcessService
A workflow that needs both declares `Flow` (or stays polymorphic with `'env :> IHasHttp` constraints) and runs against one environment value.

Portability

Request construction, the Request/Response modules, HttpError, and the DSL are portable and compile under Fable. The Http.live service and Layer.succeed (Http.live …) are .NET-only: on other hosts, implement IHttp over the platform's fetch primitive and provide it through the same environment record.

When Not To Fake

Fakes verify workflow logic, not server behavior. Keep a small number of tests against a real endpoint (a loopback listener works well) to cover the live service's encoding, header, timeout, and error mapping — the package's own test suite does exactly this.