| name | levels |
| description | テストの粒度選択のうち、1サービス内部の層(単体テスト/Unit Test、結合テスト/ Integration Test)を扱う。 test-catalog の手法カタログの一部。依存を切り離した最小単位のロジック・分岐・境界 ・例外の網羅、複数モジュールや実依存(DB、キュー、別サービス)との接続部のずれの 検証を検証したい、または割り当てたいときに使う。通常は test-catalog スキルの索引 経由で手法が選定された後にこのスキルを直接参照する。
|
| disable-model-invocation | true |
テストレベル(サービス内部: 単体・結合)
対象の粒度によるテストの分類のうち、1サービス内部の粒度(単体、結合)を扱う。
下に行くほど対象が広く、実行が遅く、本物の環境に近い。
ISO/IEC/IEEE 29119 が用語の基準だが、現場では俗称が混在するため、各手法で「何を本物にし、何を差し替えるか」を明確にして選ぶ。
サービスを箱とみなすレベル(コンポーネント、コントラクト)は levels-service.md を参照。
目次
単体テスト(Unit Test)
概要
関数、メソッド、クラスといった最小単位を、依存を切り離して検証する。
目的/いつ使う
ロジックの分岐、境界、例外を高速かつ大量に回したいときに使う。
外部 I/O やフレームワークの挙動そのものを確かめたい局面では使わない(それは結合より上の責務)。
TypeScript example
import { describe, it, expect } from "vitest";
function applyDiscount(price: number, rate: number): number {
if (rate < 0 || rate > 1) throw new RangeError("rate out of range");
return Math.round(price * (1 - rate));
}
describe("applyDiscount", () => {
it("割引を適用して四捨五入する", () => {
expect(applyDiscount(1000, 0.1)).toBe(900);
});
it("境界: rate=1 は 0 円", () => {
expect(applyDiscount(1000, 1)).toBe(0);
});
it("異常系: 範囲外の rate は例外", () => {
expect(() => applyDiscount(1000, 1.5)).toThrow(RangeError);
});
});
落とし穴
モックを積み上げて実装の呼び出し順をなぞるテストは、リファクタで即壊れる割に欠陥を捕まえない。
private メソッドを直接叩こうとするのは設計のにおい。公開された振る舞いで検証する。
網羅の定義
このレベルは「対象単位そのもののロジック網羅」を集約する層。
分岐、境界、例外の網羅はここで取り切り、上の層へ持ち上げない。
- 網羅基準(いつ網羅完了とみなすか):対象のブラックボックス技法(同値分割の全クラス各1代表 + 境界値 + 必要ならデシジョンテーブルの全規則)を満たし、かつホワイトボックスで C1(全分岐)を踏み、安全性に関わる複合条件は MC-DC を満たしたとき。
- 網羅手順(基準を満たすケース集合の作り方):1. 対象の入出力から有効/無効の同値クラスと境界を列挙する。2. 各クラスと境界に代表ケースを割り当てる。3. coverage 計測で未踏の分岐を洗い出し、それを踏むケースを追加する。
- 達成チェック(漏れの検出):外部 I/O や接続の検証がこの層に紛れていないか(それは結合へ委ねる)。逆にロジック分岐が上層へ漏れていないか(分岐網羅は単体に寄っているのが健全)。coverage の分岐到達率で未踏を検出する。
遂行手順(着手→完了)
単体の作業の本体は「対象の公開された振る舞いを列挙し、各振る舞いに手法を当て、coverage で到達を裏取りする」ことだ。次の順で進める。
- 公開振る舞いを列挙する:対象の公開関数/メソッドごとに「入力 → 観察可能な出力(戻り値・例外・状態変化)」を1行ずつ書き出す。private は対象にしない(公開振る舞い経由で踏む)。ここで挙げた行が網羅対象の母集合になる。
- 各振る舞いに手法を割り当てる:正常系は同値分割(
blackbox-partition.md)、境界は境界値、入力の組合せはデシジョンテーブル、異常系は無効同値クラスへ振る。1振る舞いに最低1ケース。
- Red→Green で1ケースずつ消す:
tdd-cycle.md に従い、列挙した行を上から1本ずつ失敗テスト→最小実装で緑にする。三角測量が要る分岐は2本目を足す。
- coverage で未踏分岐を洗う:全行を消したら C1(分岐)を計測し、未踏の分岐を踏むケースを追加する。安全性に関わる複合条件は MC-DC まで上げる。
- 越境を差し戻す:外部 I/O・接続検証がケースに紛れていたら結合へ移す。逆に分岐網羅が上層に漏れていたらここへ引き戻す。
完了チェック(もれ確認)
- 列挙した公開振る舞いが全部ケース化されたか:手順1の行数と、最低1ケース当たった振る舞いの数が一致する。差があれば振る舞いの取りこぼし。
- C1 到達率に未踏が無いか:coverage の分岐到達率を見て、未踏分岐がゼロか。残っていれば手順4が未完。
- 安全性条件が MC-DC か:金額・権限・境界判定など壊れると痛い複合条件が、各条件単独で結果を変えるケースを持っているか。
- 越境ケースが無いか:
grep 等で対象テストに外部 I/O・接続のアサート(DB/HTTP/ファイル)が無いことを確認する。あれば層違い。
結合テスト(Integration Test)
概要
複数のモジュールや、コードと実依存(DB、キュー、別サービス)の境界をまたいで検証する。
目的/いつ使う
単体では見えない接続部のずれ(SQL の方言、シリアライズ、トランザクション境界)を捕まえたいときに使う。
ロジックの全分岐を網羅する用途には向かない(遅く、組み合わせ爆発する)。
TypeScript example
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import Database from "better-sqlite3";
import { UserRepo } from "./user-repo";
let db: Database.Database;
let repo: UserRepo;
beforeAll(() => {
db = new Database(":memory:");
db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)");
repo = new UserRepo(db);
});
afterAll(() => db.close());
describe("UserRepo", () => {
it("保存した行を取得できる", () => {
const id = repo.insert("alice");
expect(repo.findById(id)?.name).toBe("alice");
});
});
落とし穴
共有 DB を使い回してテスト間で状態が漏れると、実行順で結果が変わる不安定なテストになる。
in-memory で代用すると本番 DB との方言差を見逃すことがある。重要な経路は本物のエンジンで確認する。
網羅の定義
このレベルは「モジュール境界と実依存との接続のずれ」を網羅対象に取る層。
全ロジック分岐は対象にしない(それは単体)。接続経路を代表で踏むことに集中する。
- 網羅基準(いつ網羅完了とみなすか):主要な接続経路(各実依存への読み/書き、トランザクション境界、シリアライズの往復)を代表ケースで一通り踏んだとき。分岐の全網羅は基準に含めない。
- 網羅手順(基準を満たすケース集合の作り方):1. 対象がまたぐ境界(DB、キュー、別サービス)を列挙する。2. 各境界で起こりうるずれ(SQL方言、型変換、コミット/ロールバック)を1観点ずつ代表ケースにする。3. 接続の成功経路と代表的な失敗経路を各1本ずつ用意する。
- 達成チェック(漏れの検出):共有状態(使い回す DB 等)でテスト間が汚染され実行順依存になっていないか。in-memory 代用で本物エンジンの方言差を踏み損ねていないか。重要経路は本物のエンジンで確認しているか。
遂行手順(着手→完了)
結合の作業の本体は「対象がまたぐ接続経路を数え上げ、各境界に代表ケースを1本ずつ当てる」ことだ。分岐の全網羅(=単体)に流れないよう、経路の数を先に確定させてから書く。
- 接続経路を列挙する:対象がまたぐ境界を「(モジュール/外部依存) × (操作)」の表で書き出す。例:
UserRepo × {挿入, 取得, 更新, 削除}、PaymentClient × {課金, 返金}。1セルが1経路。テストダブルへ差し替える協力者の振り分けは test-doubles.md の手順2(管理下/管理外)に従う。
- 各経路で「ずれの観点」を1つ選ぶ:その境界で単体では見えないずれ(SQL方言・型/シリアライズ変換・トランザクション境界・コミット/ロールバック)を経路ごとに1観点へ絞る。観点が経路の代表ケースを定める。同じずれを複数経路で重ねて検証しない(冗長)。
- 代表ケースを選ぶ基準を適用する:各境界につき「成功経路1本 + 業務上意味のある失敗経路1本」を代表とする。読み/書きが対称な境界は往復(書いて読み戻す)1本で両方を兼ねさせ、本数を増やさない。
- 本物エンジンで結線する:管理下プロセス外(自前 DB 等)はモックでなく本物(
test-doubles.md の Testcontainers)で状態検証する。in-memory 代用は方言差が出ない境界に限る。
- 何本で完了かを固定する:完了本数 = (境界数 × 各境界の代表本数)。手順1の表のセル数から逆算し、その本数を満たしたら打ち切る。分岐網羅をここで足したくなったら単体へ差し戻す。
完了チェック(もれ確認)
- 全境界に代表が当たったか:手順1の表の各セル(境界×操作)に、最低1本の代表ケースが対応しているか。対応の無いセルが接続経路の取りこぼし。表とテストを1対1で突き合わせる。
- 各境界が成功+失敗を持つか:境界ごとに成功経路と代表失敗経路が揃っているか(失敗だけ・成功だけは片肺)。
- 分岐網羅が紛れていないか:同一境界に3本以上のケースが付いていたら、単体で取るべき同値分割/境界値が結合へ漏れた疑い。観点が1経路1つに絞れているか見直す。
- テスト間が独立か:順序シャッフル(
vitest --sequence.shuffle 等)で緑か。落ちれば共有状態の汚染(flakiness-concurrency.md)。
- 本物エンジン一致か:重要経路が in-memory 代用でなく本番同等エンジンで踏まれているか。
関連する別ファイル