Summary#
A [Test] runs with the same capabilities the app has. Http.Get(...) inside a test makes the request, and a
typed client { } operation does too. Nothing is stubbed by default, so a test that calls your fetch function and
reads back what it stored is telling you about the real endpoint.
That matters more than it sounds, because it is the one thing the rest of the suite cannot tell you. Every other check — compile, lint, render, assert — passes just as happily when the URL is dead, the API key is missing or the response shape changed. A green suite is not evidence that the endpoint works unless a test called it.
When you want a call answered from inside the test instead — offline, deterministic, or shaped like a failure the
endpoint will not produce on demand — register a stub for that verb: Http.Get.Stub(url => …).
Signature#
Http.<Verb>.Stub(url => <HttpResponse>); // Get · Post · Put · Patch · Delete
Http.Post.Stub((url, body) => <HttpResponse>); // the second parameter is the REQUEST bodyDescription#
An app that reads a value from outside has two halves, and they fail differently:
| half | what breaks | what catches it |
|---|---|---|
| the CALL | a dead URL, a moved path, a missing key, a rate limit | only a test that makes the call |
| the PARSE | a changed response shape, a field that is now nested | a test with a canned body, or the real one |
The second half can be tested without the network: call the function that CONSUMES a response, handing it a body you wrote — or stub the fetch and let your own fetch function run over an answer you chose. The first half cannot be faked, and it is the half that fails silently in production, because a fetch that never arrives usually leaves the app showing whatever it had before.
⚠ A non-2xx is a normal return, not an exception. response.IsSuccess goes false and the body holds whatever
the server sent; nothing throws, so a try/catch around the call will not see it. Assert on IsSuccess, and
remember that some APIs answer HTTP 200 carrying an error document — a status check alone will not catch those,
but reading back what your app STORED will.
Every outbound call is logged whatever happens — a 2xx at Information, anything else at Warning, with the
method, the URL (query redacted), the status and the duration. osy logs is where a failing call explains itself.
What a failing test says about the network#
Every outbound call the test caused is also collected for the duration of that ONE test and attached to its report when it fails — a green verdict stays one line, because nobody reads the network trace of a test that passed. It is on the red one that "the fetch 404'd" and "the fetch has not arrived yet" are otherwise the same sentence.
- Both sides of the wire are in it. A call your test body makes and a call the SERVER makes answering a request
your test caused — a
Ui.Click, anApi.*— land in the same journal. Most of what a UI test does is press buttons, so this is usually the half that has the answer. - A call that never got an answer is in it too, marked
FAILED:(DNS, connect, TLS, a timeout, a body over the cap) orBLOCKED:(a target the egress guard refuses — a loopback or internal address is the common one). Those are the outcomes most easily mistaken for "it has not finished yet". - Addresses are redacted: scheme, host, port and path only. A query string is the routine carrier of an API key and a verdict gets quoted, pasted and committed.
Stubbing one verb#
Http.<Verb>.Stub(λ) registers the lambda as the deterministic implementation of that verb for the rest of the
test. It is written as a statement, like any other stub:
- The lambda takes the URL, and optionally the request body (null for
GetandDelete) — one signature for every verb, soHttp.Post.Stub((url, body) => …)reads the same way asHttp.Get.Stub(url => …). - It returns an
HttpResponse—new HttpResponse { StatusCode = 404, Body = "" }. IsSuccessis derived from the status you set, not taken from the object. A stub cannot claim a response the real client could never produce, so a test cannot assert that a 500 succeeded and pass.- A registration in a
[TestFixture]reaches every test that declares it, and a.Stublater in a test body overrides it — last registration wins. - The stubbed call still lands in the run's outbound journal, marked
(stubbed). A suite whose calls are all answered inside it must still be able to say what it would have called, or the stub becomes the new place a wrong URL hides. - It is per verb.
Http.Stub(…)is refused:Httphas five of them and there is nothing to imply.
⚑ Stubbing is opt-in on purpose, and the default is not a detail. A suite that stubs by default is a suite that cannot fail when the endpoint does — which is the failure this page exists to name. Stub the calls whose subject is the PARSE, and leave at least one test that really calls out.
Examples#
Prove the whole path — the call, the parse and the storage — by running your own fetch and reading back the row:
using Osysharp.Http;
entity ExchangeRate {
[Required] decimal GbpToEur;
security { allow read, create when IsAnonymous; }
}
void RefreshExchangeRate() {
var r = Http.Get("https://open.er-api.com/v6/latest/GBP");
new ExchangeRate { GbpToEur = 1.17m }; // …parsed from `r.Body` in a real app
UnitOfWork.Commit();
}
[Test]
void the_rate_fetch_stores_a_real_rate() {
RefreshExchangeRate(); // the app's own function: it calls out, parses, and writes
var rate = ExchangeRate.FirstOrDefault();
Assert.NotNull(rate);
Assert.True(rate.GbpToEur > 0m, "a stored rate must be a real number");
}Ask the endpoint what it actually answers, when you are not sure it is alive or keyless:
using Osysharp.Http;
[Test]
void the_endpoint_is_reachable_and_keyless() {
var r = Http.Get("https://open.er-api.com/v6/latest/GBP");
Assert.True(r.IsSuccess, "expected a 2xx");
Assert.Contains("\"rates\"", r.Body); // a 200 carrying an error document would fail HERE, not above
}Pin the response so the PARSE is asserted offline — and so the failures the endpoint will not perform on demand can be tested at all:
using Osysharp.Http;
entity ExchangeRate {
[Required] decimal GbpToEur;
security { allow read, create when IsAnonymous; }
}
void RefreshExchangeRate() {
var r = Http.Get("https://open.er-api.com/v6/latest/GBP");
if (!r.IsSuccess) { Log.Warning("rate fetch failed with {Status}", r.StatusCode); return; }
new ExchangeRate { GbpToEur = 1.17m }; // …parsed from `r.Body` in a real app
UnitOfWork.Commit();
}
[Test]
void a_good_response_is_parsed_and_stored() {
Http.Get.Stub(url => new HttpResponse { StatusCode = 200, Body = "{\"rates\":{\"EUR\":1.17}}" });
RefreshExchangeRate();
Assert.NotNull(ExchangeRate.FirstOrDefault());
}
[Test]
void a_rate_limit_stores_nothing() {
Http.Get.Stub(url => new HttpResponse { StatusCode = 429, Body = "slow down" });
RefreshExchangeRate();
Assert.Null(ExchangeRate.FirstOrDefault()); // the guard held: no half-written row
}⚠ A test that calls out depends on somebody else's uptime. That is the right trade for the one or two tests that exist to prove the integration, and the wrong one for a suite of thirty. Keep the network in the tests whose subject IS the network, stub the rest, and test everything downstream of the response against a body you supply.
See also#
- [Test] / [TestFixture] — declaring a
[Test]and what a fixture seeds - Http.* — the
Http.*surface itself - Testing (real app, real data, real rules) — the rest of the testing surface