Testing¶
Spindle.Test calls your app in-process: no socket, no port, and a clock the
test sets. It works with any test library; these examples use Alcotest.
let echo =
Spindle.post
Spindle.Path.(s "echo")
Spindle.Returns.text
(let+ body = Spindle.body in
Ok body)
let test_echo () =
let app = Spindle.Test.app [ echo ] in
let r = Spindle.Test.call app `POST "/echo" ~body:"hi" in
Alcotest.(check int) "status" 200 (Spindle_http.Status.to_int r.status);
Alcotest.(check string) "body" "hi" r.body;
Alcotest.(check (option string))
"type" (Some "text/plain; charset=utf-8")
(Spindle.Test.header r "content-type")
Test.apptakesApp.make’s arguments. It raisesInvalid_argumentfor a route tableApp.makerefuses, and for aStatic.directoryorFiles.directoryroute: load the files withStatic.loadand serve them withStatic.route, or serve a directory the test opened withFiles.route, instead.Test.app ~resourcessupplies the resources the routes read, as the server’s~resourcesdoes: a test’s own pool, say,Spindle.Test.app ~resources:[ Spindle.Dep.resource db test_pool ] routes. It needs only those the routes it calls read; calling one that reads a resource it was not given raisesInvalid_argument, naming it.Test.app ~mono_clockis the clock its rate limits are timed on: by default one that never moves. Give itEio_mock.Clock.Mono.make ()and move it withEio_mock.Clock.Mono.set_timeto wait one out. Each app counts its own, so one test’s calls never count against another’s.Test.call app meth targettakes?now(aPtime.t, defaultPtime.epoch),?peer(default127.0.0.1),?headersand?body.- To test what a route sees behind a proxy, pass
~proxied:true: the peer is then a proxy the server trusts, and~proxy_headersays which forwarding header it writes.~version:Spindle_http.Head.Http_1_0sends the request as HTTP/1.0. - The answer’s status and headers are exactly what the server would write,
cookies included, minus
connection. A stream is run to its end. r.statusis aSpindle_http.Status.t, as every status in the API is, in its one spelling: match on`Conflict, never`Code 409, or compare itsStatus.to_int.- A refusal with a code the route does not declare, or a status its route
does not list, raises
Invalid_argument: the server would only log that bug, the test fails on it.
Testing cookies and sign-in¶
Test.browser keeps the cookies the app sets, as a browser does, and sends
them on later calls:
let b = Spindle.Test.browser app in
let _ = Spindle.Test.Browser.call b `POST "/sign-in" ~body in
let me = Spindle.Test.Browser.call b `GET "/me" in ...
A cookie is dropped when the app clears it or when its age is past a later
call’s ~now. Test.Browser.cookies b lists what it holds.
Testing event streams¶
Test.events hands each event to your function until it answers `Stop.
Here, three events of the count stream:
let test_count () =
Eio_main.run @@ fun env ->
let app = Spindle.Test.app (routes ~mono_clock:(Eio.Stdenv.mono_clock env)) in
let seen = ref [] in
match
Spindle.Test.events app "/count" (fun e ->
seen := e.data :: !seen;
if List.length !seen = 3 then `Stop else `Continue)
with
| Ok () -> Alcotest.(check (list string)) "three" [ "1"; "2"; "3" ] (List.rev !seen)
| Error r -> Alcotest.failf "no stream: %d" (Spindle_http.Status.to_int r.status)
Each event has its name, data, id and retry_ms, as a client reads
them.
`Stop is the client leaving: the route’s next send is Error Gone,
and Spindle.client_gone is resolved.
Error r is the answer when the route did not stream.
Testing WebSockets and real connections¶
Test.websocket app protocol target fopens a socket to the route and runsfon the client’s end; see WebSockets.- To test over a real socket, listen on a port of your own and serve the app
with
Spindle.Server.serve_on, which runs the same loop asSpindle.Server.run.