| name | nonfunctional-resilience |
| description | 過負荷や依存劣化を封じ込める仕組みが設計どおり働くか、スキーマ移行・データ変換が情報を保存するかを 障害注入と不変条件で確かめる観点を扱う。test-catalog の手法カタログの一部。 バルクヘッド(隔離)、レートリミット、サーキットブレーカ(状態遷移の全遷移と復帰経路)、 データ品質/マイグレーション整合テスト(往復一致、行数保存、必須列の非NULL、集計一致) を検証したい、または割り当てたいときに使う。通常は test-catalog スキルの索引経由で 手法が選定された後にこのスキルを直接参照する。
|
| disable-model-invocation | true |
非機能テスト(耐障害性・データ整合)
非機能のうち「過負荷や依存劣化を封じ込める仕組みが設計どおり働くか」と「スキーマ移行・データ変換が情報を保存するか」を、障害注入と不変条件で確かめる観点を集める。
いずれも測定指標が数値レイテンシでなく「障害が区画内に閉じる」「データが保存される」のような性質の充足で表れる。
セキュリティ・a11y・互換・コントラクトなど他の品質保証系は nonfunctional-attributes.md、測定系(性能/負荷/ストレス/スパイク/ソーク/容量)は nonfunctional-perf.md を参照。
目次
耐障害性の具体技法(バルクヘッド、レートリミット、サーキットブレーカ)
可用性フェイルオーバ・カオスエンジニアリング(nonfunctional-attributes.md)が「落ちても継続するか」を見るのに対し、ここは過負荷や依存劣化を封じ込める仕組みが設計どおり働くかを、障害注入で1つずつ確かめる。共通の型は「何を注入し、何を確認するか」を対で決めること。
- バルクヘッド(隔離):ある依存への呼び出しを専用のスレッド/接続プールに隔離する。注入: その依存を全て遅延・ハングさせる。確認: プールが埋まっても他の依存への呼び出しが正常応答を返し続ける(障害が区画内に閉じ、全体に波及しない)。
- レートリミット:単位時間あたりの受付数を上限で絞る。注入: quota を超える流量を送る。確認: 上限超過分が429 Too Many Requests(または設計した拒否)で即座に返り、上限内のリクエストは成功し続ける。
Retry-After を返す設計ならその値も検証する。
- サーキットブレーカ:依存の連続失敗で回路を open にし、即座に失敗(またはフォールバック)へ切り替える。注入: 依存にエラー/遅延を閾値回数だけ与える。確認: しきい値で circuit が open に遷移し、open 中は依存を呼ばず即時にフォールバックを返す。一定時間後 half-open に移り、試行が成功すれば closed へ復帰する。
TypeScript example: サーキットブレーカ
依存に遅延を注入し、しきい値で open に遷移して即時フォールバックへ切り替わることを確認する。
import { describe, it, expect, vi } from "vitest";
import { CircuitBreaker } from "./circuit-breaker";
describe("CircuitBreaker", () => {
it("連続失敗のしきい値で open になり、以後は依存を呼ばず即フォールバック", async () => {
const dep = vi.fn().mockRejectedValue(new Error("timeout"));
const cb = new CircuitBreaker(dep, { threshold: 3, fallback: () => "fallback" });
for (let i = 0; i < 3; i++) await cb.call().catch(() => {});
expect(cb.state).toBe("open");
const callsBefore = dep.mock.calls.length;
expect(await cb.call()).toBe("fallback");
expect(dep.mock.calls.length).toBe(callsBefore);
});
});
遂行手順(着手→完了)
耐障害性の本体は「各パターンごとに『注入する障害』と『期待する遮断・隔離・拒否の挙動』を対で決め、注入して挙動を確かめる」ことだ。
- 導入済みのパターンを列挙する:対象に入っているバルクヘッド/レートリミット/サーキットブレーカを全て挙げる(設計に有るのにテストが無いものを洗う)。
- パターンごとに注入と確認を対で書く:上の一覧の「注入: / 確認:」に従い、何を壊し何を保証するかを1対1で決める。注入だけして確認が無い、はテストにならない。
- 状態遷移するものは全遷移を踏む:サーキットブレーカは closed→open→half-open→closed を全て通す。open になることだけ見て復帰(half-open→closed)を確認しないと、開きっぱなしの設計を緑にする。
- 正常時の素通りを確認する:発動しない経路(障害が無いとき素通りする)も1本持つ。常に遮断するだけの実装を見逃さないため。
- 復帰経路を確認する:half-open からの closed 復帰、レートリミット窓のリセットなど、回復側を取りこぼさない。
- 並行・状態遷移は設計レベルで固める:全 interleaving はテストで踏めないので、設計の安全性/活性は loop-engineering(TLA+)でモデル検査し、反例をテストへ落とす。
完了チェック(もれ確認)
- 注入と確認が対になっているか:各パターンに「障害注入」と「期待挙動の検証」の両方があるか。注入だけ・確認だけになっていないか。
- 全状態遷移を踏んだか:サーキットブレーカで closed→open→half-open→closed の全遷移をテストしたか。open のみで復帰未確認になっていないか。
- 正常時の素通りを見たか:障害が無いとき発動せず素通りする経路を確認したか(常時遮断バグを見逃さない)。
- 復帰経路を取りこぼしていないか:half-open 復帰やレート窓リセットなど回復側を確認したか。
- 設計の網羅を TLA+ に委ねたか:並行・状態遷移を含むなら、テストで踏めない interleaving を loop-engineering でモデル検査したか。
網羅の定義
網羅基準: 導入した各耐障害パターン(バルクヘッド/レートリミット/サーキットブレーカ)について、「注入する障害」と「期待する遮断・隔離・拒否の挙動」を対にしたケースを最低1本ずつ持ち、状態遷移を伴うもの(circuit の closed→open→half-open→closed)は全遷移を踏んだとき。達成チェック: 正常時の素通り(発動しない経路)も併せて確認しているか、復帰経路(half-open からの closed 復帰、レートリミット窓のリセット)を取りこぼしていないか。状態遷移と並行を含むため全 interleaving は踏めない。設計レベルの保証は loop-engineering(TLA+)でモデル検査し反例をテストへ落とす。
データ品質 / マイグレーション整合テスト
ISO/IEC 25010 の信頼性・互換性に隣接する観点として、スキーマ移行とデータ変換が情報を保存するかを検証する。コードのテストと別に、データそのものの不変条件を見る層。
概要
スキーマ移行(マイグレーション)の適用と巻き戻し、移行に伴うデータ変換が、データを失わず元へ戻せ、移行前後で意味を保つことを確認する。
目的/いつ使う
破壊的になりうるマイグレーション(列の追加・削除・型変更・正規化)を本番へ流す前に、ロールバック可能性とデータ保存性を担保したいときに使う。追加のみの安全な移行や、変換を伴わない単純な DDL には過剰。
不変条件(マイグレーションが守るべき性質)
- 往復一致(up→down→up):適用→巻き戻し→再適用でスキーマとデータが元の状態に戻る。down で戻せない移行は、戻せないことを明示的に検証する(沈黙の片道移行を防ぐ)。
- 行数保存:変換が 1:1 を意図するなら移行前後で対象テーブルの行数が一致する。集約・分割なら期待した比率で増減する。
- 必須列の非 NULL:新設した必須列が、既存行を含め全行で NULL でない(デフォルトやバックフィルの漏れを検出)。
- 集計一致:移行前後で意味が保たれるべき集計値(合計金額、件数、外部キーの参照整合)が一致する。
TypeScript example: 往復と行数保存
import { describe, it, expect, beforeEach } from "vitest";
import Database from "better-sqlite3";
import { up, down } from "./migrations/0002_add_status";
describe("migration 0002 の整合", () => {
let db: Database.Database;
beforeEach(() => {
db = new Database(":memory:");
db.exec("CREATE TABLE orders (id INTEGER PRIMARY KEY, total INTEGER)");
db.prepare("INSERT INTO orders (total) VALUES (?), (?)").run(100, 200);
});
it("行数を保存し、必須列がバックフィルされる", () => {
const before = db.prepare("SELECT COUNT(*) c FROM orders").get() as { c: number };
up(db);
const after = db.prepare("SELECT COUNT(*) c FROM orders").get() as { c: number };
expect(after.c).toBe(before.c);
const nulls = db.prepare("SELECT COUNT(*) c FROM orders WHERE status IS NULL").get() as { c: number };
expect(nulls.c).toBe(0);
});
it("up→down→up でスキーマが元に戻る", () => {
up(db); down(db); up(db);
expect(() => db.prepare("SELECT status FROM orders LIMIT 1").get()).not.toThrow();
});
});
落とし穴
本番に近い実エンジン・実データ分布で検証しないと、NULL 混入や型変換の桁あふれを見逃す(in-memory の代用は方言差を踏み損ねる)。down を「とりあえず空」にすると往復検証がすり抜け、巻き戻し不能が本番障害時に発覚する。
遂行手順(着手→完了)
データ品質テストの本体は「実データを入れた状態で up し、対象に関係する不変条件を検算し、down→up で往復一致を確かめる」ことだ。空テーブルで回すと最も重要なバックフィルと変換の検証がすり抜ける。
- 移行前スナップショットを取る:既存データ(空でなく実分布に近い行)を投入し、移行前の行数・集計値・対象列の状態を記録する。これが突合の基準になる。
- up を適用する:マイグレーションを当てる。本番に近い実エンジン(可能なら Testcontainers の実 DB、最低でも対象方言)で回す。in-memory 代用は方言差(型変換・制約)を踏み損ねる。
- 不変条件を検算する:対象に関係するものを全て確かめる。行数保存(または期待比率)、新設必須列が全行で非 NULL(既存行のバックフィル漏れ検出)、集計一致(合計金額・件数・外部キー参照整合)。
- down で巻き戻す:ロールバックを当て、例外なく戻ること、戻した結果が移行前スナップショットと一致することを確認する。down が「とりあえず空」になっていないか確かめる。
- down 不能なら明示的に固定する:本質的に片道の移行は、down で戻せないことを明示テストで固定する(沈黙の片道移行を防ぐ)。
- up→down→up の往復一致を確かめる:再適用してスキーマとデータが元へ戻ることをアサートし、完了とする。
完了チェック(もれ確認)
- 実データで回したか:空テーブルでなく、既存行を持つ状態で up したか(バックフィルと変換は実データでしか検証できない)。
- 実エンジンで回したか:本番方言に近い DB で検証したか。in-memory 代用で型変換の桁あふれや NULL 混入を見逃していないか。
- 対象の不変条件を全て検算したか:行数保存・非 NULL・集計一致のうち、対象に関係するものを取りこぼしていないか。変換を伴う列で集計一致を見たか。
- down が実装され往復一致するか:down が空でなく実際に巻き戻すか。up→down→up で元へ戻るか。
- 片道移行を黙って通していないか:down 不能な移行を、戻せないことの明示テストで固定したか(沈黙で通していないか)。
網羅の定義
網羅基準: 破壊的になりうる各マイグレーションについて、上の4不変条件(往復一致・行数保存・必須列非 NULL・集計一致)のうち対象に関係するものを全て検証し、down 不能な移行はそれを明示テストで固定したとき。達成チェック: 既存データを持つ状態(空テーブルでなく実データ)で回しているか、変換を伴う列で集計一致を確認しているか、片道移行を黙って通していないか。
セキュリティ・a11y・互換・コントラクトなど他の品質保証系は nonfunctional-attributes.md、測定系は nonfunctional-perf.md を参照。