# Stubbing your own functions in a test (fault injection)

> `<Function>.Stub(λ)` makes one of the app's own functions answer a value, or throw, for the rest of a `[Test]` — wherever it is called from: the test body, another function, a workflow state, a durable step, a request the test caused. It is how a test reaches the failure paths nothing in the app's own data can trigger.

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

## Summary        {#summary}

Most of what an app does wrong it does in a `catch`: the rollback that did not roll back, the failure that was logged
and never shown to anyone, the workflow that stayed in `Running` for ever. Those paths are the hardest to test, because
nothing in the app's own data makes the function they guard fail on demand.

**`<Function>.Stub(λ)`** replaces one of the app's own functions for the rest of the test. The lambda takes the
function's parameters and answers its return type — or throws, which is the point:

```osy syntax
CountLines.Stub(() => { throw new ValidationException("the archive is offline"); });
```

It is the same verb as a [client or `Http.*` stub](https://osysharp.com/reference/testing/outbound-calls/), pointed at code you wrote.

## Signature      {#signature}

```osy syntax
<Function>.Stub(<parameters> => <value>);                 // answer a value
<Function>.Stub(<parameters> => { throw new X("…"); });   // throw
<Function>.Stub(<parameters> => throw new X("…"));       // the same, as C#'s throw expression
<Function>.Stub(<parameters> => { …; return <value>; });  // a block: locals, `if`, `throw`, `return`
<VoidFunction>.Stub(<parameters> => { });                 // a `void` function: do nothing
```

## Description    {#description}

### Where does the stub apply?   {#where}

Everywhere the function runs, for the rest of the test:

| called from | answered by the stub |
|---|---|
| the test body | yes |
| another function, however deep | yes |
| a workflow state (`enter`, `on`) | yes |
| a durable step — `Workflow.Once("…", () => F())` | yes |
| a query clause — `items.Select(x => F(x))` | yes, once per element |
| a request the test caused — `Ui.Click`, `Api.*` | yes |
| a call the page answers in the browser, without asking the server | **no** — stub what it reads on the server instead |

That last row is why it lives where the other stubs live: a click is answered on a different interpreter from the one
running your test, and a stub that bit for a direct call but missed a click would be a trap.

**Register it before the thing that calls it.** An `Autostart` workflow's first state runs in the same commit that
creates its row, so a stub registered after `UnitOfWork.Commit()` is too late for it.

### What does the lambda receive, and what must it answer?   {#shape}

- **The function's own parameters**, in the order the function declares them — whatever order the caller named them
  in. A lambda with a different number of parameters is refused, quoting the signature it has to match.
- **The function's return type.** A stub for `decimal Price(string sku)` that answers a `string` is refused.
- **A block needs a `return`, unless its last statement is a `throw`** — `{ throw new X("…"); }` is the whole of the
  fault-injection shape. A `void` function's stub needs neither.
- **A thrown exception reaches the caller as the real one would**: its kind, its chain and its message. A
  `catch (ValidationException e)` catches it, and `Assert.Throws<ValidationException>(…)` sees it.

### How long does it last?   {#scope}

**Until the end of the test that registered it.** Every test runs on its own branch with its own stub table, so a test
that stubs nothing runs the real function whatever ran before it. A later `.Stub` for the same function replaces the
earlier one, and a stub in a `[TestFixture]` reaches every test that declares the fixture — which can override it.

### Can the stub call the real function?   {#call-through}

**Yes: a call to the function made from inside its own stub reaches the real one.** That is what makes the most common
fault-injection story — *the first attempt fails, the retry works* — a single `if`, rather than something a test cannot
say:

```osy syntax
Rate.Stub(currency => {
  if (failures.Count == 0) { failures.Add(currency); throw new ValidationException("the feed timed out"); }
  return Rate(currency);        // the real Rate
});
```

A stub body remembers between calls through a captured OBJECT — a `List` declared outside it — never a captured
counter: assigning to an outer variable inside a stub is refused, because a stub captures by value
([[testing-outbound-calls#what-carries-out]]).

### How many times does the body run?   {#runs-once}

**Once for each call it answers.** A stub body may note the call in a captured list, call the app's own functions —
ones that write and commit included — and test what it noted earlier; each line runs once per answered call, in order.
The same holds for the block of an `Assert.Throws(() => { … })`.

### Why is it refused outside a test?   {#test-only}

A function stub changes what a function does for **everything** that calls it. In app code that would make an
authorization helper answer `true`, or a charge answer "paid", in one line. So `.Stub` on a function is a compile error
anywhere but test code — a `[Test]`, a `[TestFixture]`, or any other function declared in a `.test.osy` file — and the
runtime refuses to register one on a run that a test did not start.

A world several tests share ("the price list answers 42 from now") is therefore written once, as a plain function in
the test file, and each test calls it; the stub it registers lasts for the calling test, exactly as one written inline.

Also refused, each by name: a stub for a function that does not exist (with the nearest one that does), a `[Test]`
function, a generic function, and a function taking a `Query<T>` — that one is inlined into each caller at compile
time, so there is no call left to answer.

⚠ **Only top-level functions.** A method on a `class` and a component member are not stubbable; stub the top-level
function that calls them, or that they call.

## Examples       {#examples}

A nightly report that must not stay stuck when its counting fails — the failure path, proven:

```osy title="make a function fail, and assert what the workflow does about it" run app=function-stubs-make-it-fail
using Osysharp.Workflow;

enum ReportState { Running, Done, Failed }

entity NightlyReport {
  int Lines = 0;
  [MaxLength(400)] string? FailureReason;
  ReportState State;
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}

int CountLines() {
  return 12;
}

workflow NightlyReportFlow {
  Tracks    = NightlyReport.State;
  Autostart = true;
  Initial   = Running;

  state Running {
    enter {
      try {
        this.Item.Lines = CountLines();
      } catch (Exception e) {
        this.Item.FailureReason = e.Message;
        goto Failed;
      }
      goto Done;
    }
  }
  terminal success Done { }
  terminal error Failed { }
}

[Test]
void a_report_that_cannot_count_records_why_and_fails() {
  CountLines.Stub(() => { throw new ValidationException("the archive is offline"); });   // before the row: Autostart
  new NightlyReport { };
  UnitOfWork.Commit();
  var report = NightlyReport.Single();
  Assert.Equal(ReportState.Failed, report.State);
  Assert.Contains("offline", report.FailureReason);
}

[Test]
void without_a_stub_the_real_count_runs() {
  new NightlyReport { };
  UnitOfWork.Commit();
  Assert.Equal(12, NightlyReport.Single().Lines);
}
```

Answer a value for every caller, and fail once before recovering:

```osy title="answer a value, and fail the first call before reaching the real function" run app=function-stubs-answer-and-recover
decimal Rate(string currency) {
  return 1.10m;
}

decimal ToEuros(decimal amount, string currency) {
  return amount * Rate(currency);
}

[Test]
void a_stub_answers_for_every_caller() {
  Rate.Stub(currency => 2m);
  Assert.Equal(20m, ToEuros(10m, "GBP"));
}

[Test]
void the_first_call_fails_and_the_next_reaches_the_real_rate() {
  var failures = new List<string>();
  Rate.Stub(currency => {
    if (failures.Count == 0) {
      failures.Add(currency);
      throw new ValidationException("the feed timed out");
    }
    return Rate(currency);
  });
  Assert.Throws<ValidationException>(() => ToEuros(10m, "GBP"));
  Assert.Equal(11m, ToEuros(10m, "GBP"));
}
```

## See also       {#see-also}

- [Outbound calls in a test](https://osysharp.com/reference/testing/outbound-calls/) — the same verb for what leaves the process: `Http.<Verb>.Stub`, which answers every
  typed client method built on that verb at once
- [[Test] / [TestFixture]](https://osysharp.com/reference/testing/test/) — declaring a `[Test]`, and what a `[TestFixture]` carries into it
- [Assert](https://osysharp.com/reference/testing/assert/) — `Assert.Throws<T>` for asserting the fault reaches the caller
- [Workflow.Once (run a step at most once)](https://osysharp.com/reference/workflow/once/) — a durable step, which a function stub reaches like any other call
- [Testing (real app, real data, real rules)](https://osysharp.com/reference/testing/index/) — the rest of the testing surface
