# 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.

<!-- id: testing-outbound-calls · area: testing · stability: preview · html: https://osysharp.com/reference/testing/outbound-calls/ -->

## Summary        {#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      {#signature}

```osy syntax
Http.<Verb>.Stub(url => <HttpResponse>);              // Get · Post · Put · Patch · Delete
Http.Post.Stub((url, body) => <HttpResponse>);        // the second parameter is the REQUEST body
```

## Description    {#description}

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   {#the-journal}

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   {#stubbing}

`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 `HttpResponse`** — `new 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       {#examples}

Prove the whole path — the call, the parse and the storage — by running your own fetch and reading back the row:

```osy title="prove the whole path — call, parse and storage" test app=outbound-calls-whole-path
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:

```osy title="ask the endpoint what it actually answers" test app=outbound-calls-probe-the-endpoint
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:

```osy title="stub the fetch to assert the parse, and the failures" run app=outbound-calls-stub-the-response
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       {#see-also}

- [[Test] / [TestFixture]](https://osysharp.com/reference/testing/test/) — declaring a `[Test]` and what a fixture seeds
- [Http.*](https://osysharp.com/reference/http/facade/) — the `Http.*` surface itself
- [Testing (real app, real data, real rules)](https://osysharp.com/reference/testing/index/) — the rest of the testing surface
