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.
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. The first half cannot — 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.
Examples#
Prove the whole path — the call, the parse and the storage — by running your own fetch and reading back the row:
using Osyrin.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 Osyrin.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
}⚠ 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, 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