Osy#betaa language · its runtime Osyrin · a hosted platform
Why Osy#Built for agentsAgents as declarationsWorkflows that waitRuns exactly onceSecure by defaultNothing to mockThe editor is the compilerUI in the languageDocuments are dataOne program

Reference / Testing

Outbound calls in a test

A `[Test]` reaches the network for real — `Http.*` and a typed `client { }` operation both run inside a test, against the live endpoint. So a test can prove the one thing a green suite otherwise cannot: that the call your app depends on actually works. `Http.<Verb>.Stub(url => …)` opts one verb out, for the assertions the network cannot give you on demand.

preview3 examples compiled by CItestinghttpintegration

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 body

Description#

An app that reads a value from outside has two halves, and they fail differently:

halfwhat breakswhat catches it
the CALLa dead URL, a moved path, a missing key, a rate limitonly a test that makes the call
the PARSEa changed response shape, a field that is now nesteda 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, an Api.* — 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) or BLOCKED: (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 Get and Delete) — one signature for every verb, so Http.Post.Stub((url, body) => …) reads the same way as Http.Get.Stub(url => …).
  • It returns an HttpResponsenew HttpResponse { StatusCode = 404, Body = "" }.
  • IsSuccess is 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 .Stub later 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: Http has 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#

Related

[Test] / [TestFixture]

A test is an ordinary function marked [Test]. It runs against a throwaway clone of the app, so it may create rows and…

Testing (real app, real data, real rules)

A test in Osy# is not a unit test with the world mocked out. It is your app, running against its own throwaway…

Http.*

Make an outbound HTTP call to a URL you build at runtime — a webhook, a third-party API, a discovered endpoint…