> ## Documentation Index
> Fetch the complete documentation index at: https://lib.findy.co.jp/llms.txt
> Use this file to discover all available pages before exploring further.

# Test Code

> Write test code that protects the system — tests that pass when they should pass and fail when they should fail — instead of chasing coverage numbers.

## Overview

Test code verifies that the system behaves as intended, and it is the safety net that makes continuous change possible.

What matters in test code is that it **protects the system**: a protective test passes when the behavior is correct and fails when the behavior breaks. Its value is not in every test passing, but in preventing unintended behavior before it is released to production.

This page explains the principles behind such tests and shows patterns for writing protective tests.

## Principles

### The purpose of tests is sustainable growth of the software

Automated tests exist to make the growth of the software sustainable: it keeps the project in a state where the effect of a change can be verified with trustworthy results in a short time, so the team can keep changing the system with confidence. Detecting unintended behavior is the mechanism, not the end goal.

Quality and speed are therefore not a trade-off. Skipping tests buys a short-lived head start, and the cost returns quickly as unintended behavior slipping in, manual re-verification, and fear of touching existing code. It is not that there is no time to write tests — there is no time because tests are not written.

Refactoring and incremental improvement require confidence that existing behavior still holds after a change, and test code provides it.

This matters even more when AI coding tools propose changes: whether a proposed change is safe to accept is ultimately answered by the test code, and the quality of the test code determines how much of that work can be delegated.

### Passing and failing must both be trustworthy

Test code protects the system only while its results can be trusted in both directions. Two kinds of failures erode trust in test code:

<CardGroup cols={2}>
  <Card title="False negative — broken but passing" icon="eye-slash">
    The behavior is broken but the test passes. Fragmentary assertions or mocking the logic under test cause it; the test reports safety that does not exist, and unintended behavior is released to production.
  </Card>

  <Card title="False positive — correct but failing" icon="triangle-exclamation">
    The behavior is correct but the test fails. Dependence on implementation details or unstable timing causes it; frequent false positives train the team to ignore failures, until the moment the behavior truly breaks is missed as well.
  </Card>
</CardGroup>

Strengthen assertions to reduce false negatives, and assert observable behavior rather than internal implementation to reduce false positives. Test code earns trust by keeping both low. The patterns later in this page guard mainly against false negatives.

### Coverage is a result, not a goal

Coverage measures which lines test code executes, not whether it asserts the right behavior. A line can run without any assertion that it does the correct thing, so a high percentage can coexist with tests that protect very little.

The reliable order is to decide **what behavior must not break**, then add test cases that would catch it breaking. Coverage rises as a consequence.

### Write tests in the same pull request as the implementation

A behavior change and the test that protects it belong in the same pull request. Deferring the test to a follow-up leaves a window where the behavior is unprotected, and in practice the follow-up competes with new work and loses.

Review test code at least as rigorously as implementation code: a weak assertion approved today is unintended behavior missed later.

See [Pull Requests](/development/pull-request) for how to keep such changes reviewable.

## Writing tests that protect

The following patterns are cases where a test looks fine but would not catch the bug it is meant to catch — that is, sources of false negatives. Each pattern strengthens the test until it fails for the failures that matter.

The same patterns are the recurring weaknesses of test code generated by AI tools: generated tests tend to optimize for a green run rather than for protection, so review them against these patterns before merging, with the same scrutiny as hand-written tests.

### 1. Assert execution state, not just execution

Asserting only that a callback was called hides three bugs: a handler that fires twice when it should fire once, a handler called with the wrong arguments (another user's data, for example), and an error-path callback that runs when it should never run. Pin down the count and the arguments, and assert that the handlers that should never be called were never invoked.

<CodeGroup>
  ```javascript Jest theme={null}
  // ❌ Only asserts the callback was called
  // (double fire, wrong payload, and onError all pass)
  expect(onSuccess).toHaveBeenCalled();

  // ✅ Pin the count, the arguments, and the silent paths
  expect(onSuccess).toHaveBeenCalledTimes(1);
  expect(onSuccess).toHaveBeenCalledWith({ userId: 123 });
  expect(onError).not.toHaveBeenCalled();
  ```

  ```ruby RSpec theme={null}
  # ❌ Only asserts the callback was called
  # (double call, wrong payload, and on_error all pass)
  expect(on_success).to have_received(:call)

  # ✅ Pin the count, the arguments, and the silent paths
  expect(on_success).to have_received(:call).with(user_id: 123).once
  expect(on_error).not_to have_received(:call)
  ```
</CodeGroup>

### 2. Assert the whole expected value, not fragments

Checking one field of a result leaves every other field unprotected: an unchecked field that is dropped, renamed, or corrupted passes silently. Compare the entire structure against a literal expected value, so any unintended change to the shape or content of the result fails the test.

<CodeGroup>
  ```javascript Jest theme={null}
  // ❌ Only asserts individual fields
  expect(result.id).toBe(1);
  expect(result.name).toBe("Alice");
  expect(result.role).toBe("member");

  // ✅ Pin the whole structure (any unintended change fails)
  expect(result).toStrictEqual({ id: 1, name: "Alice", role: "member" });

  // ✅ Check the keys in addition to individual fields
  // (missing or extra keys — dropped or unintentionally added fields — also fail)
  expect(Object.keys(result).sort()).toEqual(["id", "name", "role"]);
  expect(result.id).toBe(1);
  expect(result.name).toBe("Alice");
  expect(result.role).toBe("member");
  ```

  ```ruby RSpec theme={null}
  # ❌ Only asserts individual attributes
  expect(result[:id]).to eq(1)
  expect(result[:name]).to eq("Alice")
  expect(result[:role]).to eq("member")

  # ✅ Pin the whole structure (any unintended change fails)
  expect(result).to eq({ id: 1, name: "Alice", role: "member" })

  # ✅ Check the keys in addition to individual attributes
  # (missing or extra keys — dropped or unintentionally added fields — also fail)
  expect(result.keys).to contain_exactly(:id, :name, :role)
  expect(result[:id]).to eq(1)
  expect(result[:name]).to eq("Alice")
  expect(result[:role]).to eq("member")
  ```
</CodeGroup>

Generated tests in particular often assert just enough for the current behavior to pass: the tests pass today, but there is no guarantee against a future extension that breaks an unasserted part of the behavior.

<CodeGroup>
  ```javascript Jest theme={null}
  // ❌ Asserts only the count (any two items pass)
  expect(result.items.length).toBe(2);

  // ✅ Pin the whole expected value (any broken item, price, or total fails)
  expect(result).toStrictEqual({
    items: [
      { id: 1, name: "Coffee", price: 500 },
      { id: 2, name: "Beans", price: 1200 },
    ],
    total: 1700,
  });
  ```

  ```ruby RSpec theme={null}
  # ❌ Asserts only the count (any two items pass)
  expect(result[:items].length).to eq(2)

  # ✅ Pin the whole expected value (any broken item, price, or total fails)
  expect(result).to eq({
    items: [
      { id: 1, name: "Coffee", price: 500 },
      { id: 2, name: "Beans", price: 1200 }
    ],
    total: 1700
  })
  ```
</CodeGroup>

Loose matchers weaken assertions the same way. `toBeTruthy` and `be_truthy` pass for a non-empty string, a number, or any other truthy value, so a refactor that quietly changes a boolean return into another type keeps passing. Assert the exact value and type instead.

<CodeGroup>
  ```javascript Jest theme={null}
  // ❌ Passes even when the value is not true ("yes" or 1 also passes)
  expect(active).toBeTruthy();

  // ✅ Assert the exact value and type
  expect(active).toBe(true);
  expect(count).toBe(0);
  ```

  ```ruby RSpec theme={null}
  # ❌ Passes even when the value is not true ("yes" or 1 also passes)
  expect(active?).to be_truthy

  # ✅ Assert the exact value and type
  expect(active?).to be true
  expect(count).to eq 0
  ```
</CodeGroup>

### 3. Mock the boundary, not the logic under test

When a green run becomes the goal, an AI coding tool may mock the very logic under test, or adjust the expected value to whatever the implementation currently returns. Such a test passes even if the implementation is wrong, because the implementation itself was treated as the answer key.

The proper role of a mock is the opposite: fixing an external boundary — the current time, a network response — so that the output becomes deterministic. Output that depends on the current time, such as a mail body that embeds an expiry date, can only be asserted in fragments until the clock is pinned; once the time is mocked, the whole expected value can be asserted exactly, as in pattern 2.

Describe how mocks should be used in each case in the project's custom instructions, and verify during review that the expected values come from the specification, not from the output.

<CodeGroup>
  ```javascript Jest theme={null}
  // Implementation under test: builds a mail body whose expiry date
  // is seven days from the current time
  const mailService = {
    createInviteMailBody(toEmail, inviteLink) {
      const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
      const expiryDate = expiresAt.toISOString().slice(0, 10); // e.g. "2026-07-08"
      return `Dear ${toEmail},\n${inviteLink}\nThis link expires on ${expiryDate}.`;
    },
  };

  const expectedBody = `Dear ${toEmail},\n${inviteLink}\nThis link expires on 2026-07-08.`;

  // ❌ Mocks the logic under test itself
  // (only asserts the mock's return value — a broken implementation passes)
  jest.spyOn(mailService, "createInviteMailBody").mockReturnValue(expectedBody);
  expect(mailService.createInviteMailBody(toEmail, inviteLink)).toBe(expectedBody);

  // ✅ Mock only the external boundary (the clock), run the real logic
  // (the expiry is pinned to 2026-07-08 — a bug in the implementation fails)
  jest.useFakeTimers().setSystemTime(new Date("2026-07-01T00:00:00Z"));
  expect(mailService.createInviteMailBody(toEmail, inviteLink)).toBe(expectedBody);
  ```

  ```ruby RSpec theme={null}
  # Implementation under test: builds a mail body whose expiry date
  # is seven days from the current time
  class MailService
    def create_invite_mail_body(to_email, invite_link)
      expiry_date = (Time.zone.now + 7.days).strftime("%Y-%m-%d") # e.g. "2026-07-08"
      "Dear #{to_email},\n#{invite_link}\nThis link expires on #{expiry_date}."
    end
  end

  expected_body = "Dear #{to_email},\n#{invite_link}\nThis link expires on 2026-07-08."

  # ❌ Mocks the logic under test itself
  # (only asserts the mock's return value — a broken implementation passes)
  allow(mail_service).to receive(:create_invite_mail_body).and_return(expected_body)
  expect(mail_service.create_invite_mail_body(to_email, invite_link)).to eq(expected_body)

  # ✅ Mock only the external boundary (the clock), run the real logic
  # (the expiry is pinned to 2026-07-08 — a bug in the implementation fails)
  travel_to Time.zone.local(2026, 7, 1) do
    expect(mail_service.create_invite_mail_body(to_email, invite_link)).to eq(expected_body)
  end
  ```
</CodeGroup>

## Related pages

<CardGroup cols={2}>
  <Card title="Pull Requests" icon="code-pull-request" href="/development/pull-request">
    Ship the test in the same reviewable unit as the change it protects.
  </Card>

  <Card title="Vibe Coding" icon="wand-magic-sparkles" href="/ai/vibe-coding">
    Delegate implementation to AI tools on top of a suite you trust.
  </Card>
</CardGroup>
