故障矩阵

Writing invariants

An invariant is the rule the fault matrix checks after every trial. Write one by comparing your app with the provider.

本页还没有中文版,下面是英文原文。

An invariant is a check that must be true however a test went: whether the payment succeeded, failed or was retried. You attach it to a test with .invariant(). The fault matrix runs it after every trial.

The shape of an invariant

.invariant() takes a function with the same fixtures as the test. It runs after the test's body, once background work like webhooks has finished:

tests/buy-credits.danwo.ts
import { type StripeFixtures, test as base } from "@danwo/sdk"

const test = base.extend<StripeFixtures>()

test("buys 500 credits", async ({ app, screen, expect }) => {
  await app.open("/")
  await screen.getByRole("button", { name: "Buy 500 credits" }).click()
  await screen.getByTestId("checkout-pay").click()
}).invariant(async ({ app, stripe, expect }) => {
  const res = await stripe.fetch(
    new Request("https://api.stripe.com/v1/checkout/sessions?payment_status=paid", {
      headers: { authorization: "Bearer sk_test_any" },
    }),
  )
  const { data } = await res.json()
  const { credits } = await (await fetch(new URL("/api/credits", app.baseUrl))).json()

  expect(credits).toBe(data.length * 500)
})

Compare two sides

A good invariant reads the same fact from two places and checks they agree:

Your app saysThe provider saysInvariant
credits grantedpayments Stripe capturedequal
orders confirmedconfirmation emails Resend acceptedone email per order
replies postedmessages in the Slack channelone reply per mention

Read your app's side through its own API or database. Read the provider's side through its emulator fixture.

To query your app's database from an invariant, see Your app's data in tests.

Reading the provider's side

Each emulator gives your test a fixture named after it. Ask it what your app did:

tests/order-email.danwo.ts
import { type ResendFixtures, resendInbox, test as base } from "@danwo/sdk"

const test = base.extend<ResendFixtures>()

test("confirms the order by email", async ({ app, screen }) => {
  await app.open("/checkout")
  await screen.getByRole("button", { name: "Place order" }).click()
}).invariant(async ({ resend, expect }) => {
  expect(resendInbox(resend).list("[email protected]")).toHaveLength(1)
})

Every provider's page lists its fixture and helpers. See Providers.

Rules of thumb

  • Compare counts and totals, not the screen. The screen may never load in a trial where a call failed. The invariant still has to hold.
  • Allow a clean failure. If the payment failed, zero credits and zero charges is correct. Write the rule so both outcomes pass.
  • Never compare times or ids. They change on every trial.

A test without an invariant can still go through the matrix. Then the test itself must pass under every fault, which is a much stricter bar.

Next

本页内容

在每个 Pull Request 上运行

每个 Pull Request 一个 GitHub Check 和一条评论,并附上完整的运行记录。

配置 CI