# Testing

Test in three steps, from no account to real gateways in test mode.

## 1. The test gateway, in your test suite

No account, no network, no money. Your handler runs exactly as it will in production.

```ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { CallCash } from "callcash";

test("a paid order is fulfilled once", async () => {
  const cash = new CallCash();
  const fulfilled: string[] = [];
  const onCash = async (e) => { if (e.type === "payment.paid") fulfilled.push(e.reference); };

  const p = await cash.payments.create({ amount: 250, reference: "order_1" });
  await cash.test.pay(p.id, onCash);

  assert.deepEqual(fulfilled, ["order_1"]);
});
```

Rehearse what goes wrong:

| Call | Your handler gets |
|---|---|
| `cash.test.pay(id, onCash)` | `payment.paid` |
| `cash.test.fail(id, onCash, "insufficient funds")` | `payment.failed` with that reason |
| `cash.test.pay(id, onCash, { amount: 2.5 })` | `payment.mismatch`, and the payment stays unpaid |
| `cash.test.pay(id, onCash)` twice on a paid payment | `payment.mismatch`: paid a second time |
| A handler that throws | The event stays owed; `cash.events.deliver(onCash)` brings it back with `redelivery: true` |

`cash.test.pay` and `cash.test.fail` only work on payments made on the test gateway.

## 2. Yoco test keys

Configure Yoco with an `sk_test_` key. Payments open a real Yoco checkout and Yoco's test cards work on it. Events arrive at your webhook route with `mode: "test"`. Refunds are not possible on test payments.

Yoco must be able to reach your webhook route, so run your app on a public address (a preview deployment, or a tunnel to your machine).

## 3. Netcash test mode

Tick "Make test mode active" in the Netcash dashboard and set `test: true`. Keep test amounts **under R100**: a new Pay Now service hides the card option above that limit.

Netcash calls the Notify URL you typed into NetConnector, so that address must be public too.

## Testing your own store

If you write your own `Store`, test that two `update` calls at the same moment cannot both win. That atomic step is what stops a payment counting twice.
