概要
| 項目 | 内容 |
|---|
| 案件ID | MAS-196 |
| カテゴリ | テスト |
| Phase | P1 |
| 優先度 | ★★ |
| 所要時間 | 1時間 |
| 対象ファイル | 900_test/901_test_runner.js |
| テスト対象 | 000_infra/003_contracts.js
200_data/202_repository.js
000_infra/004_utils.js |
| スコープ外 | 200_data/201_data_validator.js(シート操作と密結合のため除外) |
目的
Modular Monolith の基盤層(Contracts / Repository / Utils)にユニットテスト・統合テストを追加し、リファクタリングや機能追加時の回帰を検知できるようにする。
Repository 層は本番データ破損リスクを回避するため 読み取りテストのみ とし、save() / append() のテストはスコープ外とする。Contracts 層・Utils 層は純粋関数のため、シートに依存しないインメモリテストで検証する。
テストID の割り当てについて
既存テストランナーの最終テストIDは T8(Pipeline RPA)。MAS-098 仕様書では T9 を冪等性テストとして予約しているが、実コードでは T7 が既に冪等性を網羅的にカバーしている(T7-01〜T7-09)。本案件では MAS-098 の T9 予約と競合しない前提で T9〜T11 を割り当てる。MAS-098 を別途実装する場合はID調整が必要。
テスト仕様
テストスイート T9: Contracts 層
| # | テスト名 | 対象関数 | 手順 | 期待結果 |
|---|
| T9-01 | toDto: 基本変換 | Contracts.toDto | headers=['A','B','C'], row=[1,'x',true] を変換 | {A:1, B:'x', C:true} |
| T9-02 | toRow: 基本変換 | Contracts.toRow | headers=['A','B','C'], dto={A:1, B:'x', C:true} を変換 | [1,'x',true] |
| T9-03 | ラウンドトリップ | toDto → toRow | headers と row から toDto → toRow と往復変換 | 元の row と一致(JSON.stringify 比較) |
| T9-04 | toDto: 空ヘッダーキーのスキップ | Contracts.toDto | headers=['A','','C'], row=[1,2,3] を変換 | {A:1, C:3}(空キー '' のプロパティが存在しない) |
| T9-05 | toRow: DTO に存在しないヘッダー | Contracts.toRow | headers=['A','B','C'], dto={A:1} を変換 | [1,'',''](未定義キーは空文字) |
| T9-06 | toDtoList: 複数行変換 | Contracts.toDtoList | data=[['A','B'],[1,2],[3,4]](ヘッダー+2行)を変換 | headers=['A','B'], rows の長さ=2, 各行が正しい DTO |
| T9-07 | toDtoList: ヘッダーのみ(データ0行) | Contracts.toDtoList | data=[['A','B']] を変換 | headers=['A','B'], rows の長さ=0 |
| T9-08 | toRows: 複数DTO一括変換 | Contracts.toRows | 2件のDTOを一括変換 | 各行が toRow 単体と同じ結果 |
テストスイート T10: Repository 層(読み取り専用)
| # | テスト名 | 対象関数 | 手順 | 期待結果 |
|---|
| T10-01 | OrderRepository.findAll: 戻り値構造 | OrderRepository.findAll | findAll() を呼び出し | 戻り値に headers(配列) と dtos(配列) が存在 |
| T10-02 | OrderRepository.findAll: ヘッダーに必須列 | OrderRepository.findAll | headers に '発注ID(ORD)' が含まれるか | indexOf !== -1 |
| T10-03 | InvoiceRepository.findAll: DTO プロパティ存在 | InvoiceRepository.findAll | 先頭DTOに '請求ID(INV)', '科目名', '収支区分' が存在するか | 全て hasOwnProperty === true |
| T10-04 | BankTxRepository.findAll: 戻り値構造 | BankTxRepository.findAll | findAll() の戻り値を検証 | headers と dtos が配列 |
| T10-05 | JournalRepository.findAll: 戻り値構造 | JournalRepository.findAll | findAll() の戻り値を検証 | headers と dtos が配列 |
| T10-06 | AccountRepository.findAsMap: マップ構造 | AccountRepository.findAsMap | findAsMap() の戻り値を検証 | オブジェクト型、各値に stmt と cat プロパティが存在 |
| T10-07 | AccountRepository.findAsMap: キャッシュ有効性 | AccountRepository.findAsMap | resetCache() → findAsMap() を2回呼び出し | 2回目の結果が1回目と同一(JSON.stringify 比較) |
| T10-08 | AccountRepository.findAsMap: 有効フラグ=FALSE 除外 | AccountRepository.findAsMap | findAll() で有効フラグ=FALSE のレコードの科目名を取得し、findAsMap() にそのキーが無いことを確認 | マップにキーが存在しない |
テストスイート T11: Utils 層
| # | テスト名 | 対象関数 | 手順 | 期待結果 |
|---|
| T11-01 | parseDateToYm: Date型 | Utils.parseDateToYm | new Date(2025, 0, 15) を変換 | '2025-01' |
| T11-02 | parseDateToYm: スラッシュ形式 | Utils.parseDateToYm | '2025/03' を変換 | '2025-03' |
| T11-03 | parseDateToYm: 和暦風形式 | Utils.parseDateToYm | '2025年3月' を変換 | '2025-03' |
| T11-04 | parseDateToYm: 空値 | Utils.parseDateToYm | null, '', undefined を変換 | 全て '' |
| T11-05 | parseDateToYmd: 日部分なしフォールバック | Utils.parseDateToYmd | '2025-03' を変換 | '2025-03-01'(日部分に -01 を付与) |
| T11-06 | addMonths: 年跨ぎ加算 | Utils.addMonths | '2025-11' に +3 | '2026-02' |
| T11-07 | addMonths: 年跨ぎ減算 | Utils.addMonths | '2026-02' に -3 | '2025-11' |
| T11-08 | parseAmt: 全角数字 | Utils.parseAmt | '12,345' を変換 | 12345 |
| T11-09 | parseAmt: カンマ区切り | Utils.parseAmt | '1,234,567' を変換 | 1234567 |
| T11-10 | parseAmt: 負数・記号混在 | Utils.parseAmt | '▲-500円' を変換 | -500 |
| T11-11 | parseAmt: 数値型パススルー | Utils.parseAmt | 42 を変換 | 42(そのまま返却) |
| T11-12 | adjustToBusinessDay: 月末クランプ | Utils.adjustToBusinessDay | '2025-11-31'(存在しない日付)を変換 | '2025-11-28'(11月30日=日曜 → 前営業日の28日金曜) |
実装方針
テストランナーへの組み込み
901_test_runner.js に以下の3関数を追加し、runAllTests() 内の T8 ブロックの後に try-catch で呼び出す。
// T9: Contracts 層
try { testT9_Contracts_(); }
catch (e) { addResult_('T9-ERR', 'Contracts: 例外', false, 'no error', e.message); }
// T10: Repository 層
try { testT10_Repository_(); }
catch (e) { addResult_('T10-ERR', 'Repository: 例外', false, 'no error', e.message); }
// T11: Utils 層
try { testT11_Utils_(); }
catch (e) { addResult_('T11-ERR', 'Utils: 例外', false, 'no error', e.message); }
テスト関数の命名規則
| 関数名 | テストID | 対象 |
|---|
testT9_Contracts_() | T9-01〜T9-08 | Contracts 層 |
testT10_Repository_() | T10-01〜T10-08 | Repository 層 |
testT11_Utils_() | T11-01〜T11-12 | Utils 層 |
テストの独立性
- T9〜T11 は相互に順序非依存。いずれかが例外で落ちても、try-catch により後続テストに影響しない
- T1〜T8(既存テスト)とも独立。RPA実行やシート書き込みを一切行わない
- T10 のみ実シートを読み取るが、データの変更は行わない
アサーション手法(GAS固有)
GAS にはアサーションライブラリがないため、以下の方法で比較する。
// プリミティブ値: === で比較
addResult_('T9-01', 'toDto: 基本変換', dto['A'] === 1, 1, dto['A']);
// オブジェクト・配列: JSON.stringify で比較
var expected = JSON.stringify([1, 'x', true]);
var actual = JSON.stringify(row);
addResult_('T9-03', 'ラウンドトリップ', expected === actual, expected, actual);
// プロパティ存在チェック: hasOwnProperty
addResult_('T10-03', 'DTO プロパティ', dto.hasOwnProperty('請求ID(INV)'), true, dto.hasOwnProperty('請求ID(INV)'));
// 型チェック: Array.isArray, typeof
addResult_('T10-01', '戻り値構造', Array.isArray(result.headers), true, Array.isArray(result.headers));
注意事項
- 書き込み禁止: Repository 層の
save() / append() はテストしない。本番データ破損リスクがあるため、読み取り (findAll / findAsMap) のみ検証する
- dev 環境限定: T10(Repository 層)は実シートを読み取るため、dev 環境でのみ実行すること。prod 環境ではシートが異なりテスト結果が変わる可能性がある
- キャッシュのリセット: T10 で
AccountRepository.findAsMap() をテストする前後に AccountRepository.resetCache() を必ず呼び出し、他テストへの副作用を防ぐ
- 固定値検証の回避: T10 では環境差異でテストが落ちないよう、特定のデータ値(科目名や金額)ではなく、型・プロパティ存在・配列長 > 0 などのスキーマ検証を行う
- prod デプロイ時の自動削除:
900_test/ ディレクトリは prod デプロイ時に GitHub Actions で自動削除されるため、テストコードが本番に混入するリスクはない
関連ドキュメント
人間が検討すべき事項
- MAS-098(T9 予約)との ID 調整方針。T7 で冪等性が十分なら MAS-098 を完了扱いにして T9 を MAS-196 に割り当て可能
- T10-08(有効フラグ=FALSE 除外テスト)は dev 環境に該当データが存在しない場合スキップされる。テストデータの整備要否
実装プロンプト(Claude Code 用)
以下のプロンプトを Claude Code にコピペして実行する。
## 背景
案件 N-20「Repository/Contracts/Utils 層テスト追加」の実装。
GAS会計システムの基盤層に対するユニットテスト・統合テストをテストランナーに追加する。
## 実装前の必須確認
以下のファイルを読み込み、関数シグネチャ・戻り値の構造を把握すること:
- `000_infra/003_contracts.js` — Contracts.toDto/toDtoList/toRow/toRows のシグネチャ
- `200_data/202_repository.js` — 各Repository.findAll()の戻り値構造、AccountRepository.findAsMap()の挙動
- `000_infra/004_utils.js` — Utils.parseDateToYm/parseDateToYmd/addMonths/parseAmt/adjustToBusinessDay のシグネチャ
- `900_test/901_test_runner.js` — addResult_() の引数、runAllTests() の構造
- `CLAUDE.md` — テストランナーの配置規約
## 修正対象ファイル
`900_test/901_test_runner.js` への追記のみ。新規ファイル作成不要。
## 実装内容
### 1. テスト関数の追加(3関数)
runAllTests() の前に以下の3関数を追加する:
**testT9_Contracts_()**
- T9-01〜T9-08: Contracts 層のインメモリテスト
- ヘッダー配列と行配列を手動構築し、toDto/toRow/toDtoList/toRows の変換結果を検証
- 空ヘッダーキーのスキップ、DTO に存在しないヘッダーへの空文字埋め、ラウンドトリップを検証
- 比較には JSON.stringify を使用
**testT10_Repository_()**
- T10-01〜T10-08: Repository 層の読み取りテスト
- 各Repository.findAll() の戻り値が {headers:[], dtos:[]} 構造であることを検証
- AccountRepository.findAsMap() のキャッシュ有効性と有効フラグ=FALSE の除外を検証
- **重要**: resetCache() をテスト開始時と終了時に呼ぶこと
- **重要**: save()/append() は絶対に呼ばないこと(本番データ破損リスク)
- 固定値検証は避け、型・プロパティ存在・配列長のスキーマ検証に留める
**testT11_Utils_()**
- T11-01〜T11-12: Utils 層のインメモリテスト
- parseDateToYm: Date型, スラッシュ形式, 和暦風形式, 空値
- parseDateToYmd: 日部分なしフォールバック("2025-03" → "2025-03-01")
- addMonths: 年跨ぎ加算・減算
- parseAmt: 全角数字, カンマ区切り, 負数・記号混在, 数値型パススルー
- adjustToBusinessDay: 存在しない日付 "2025-11-31" の月末クランプ+営業日調整
### 2. runAllTests() への組み込み
T8 ブロックの後に T9〜T11 を try-catch で追加する:
```js
// T9: Contracts 層
try { testT9_Contracts_(); }
catch (e) { addResult_('T9-ERR', 'Contracts: 例外', false, 'no error', e.message); }
// T10: Repository 層
try { testT10_Repository_(); }
catch (e) { addResult_('T10-ERR', 'Repository: 例外', false, 'no error', e.message); }
// T11: Utils 層
try { testT11_Utils_(); }
catch (e) { addResult_('T11-ERR', 'Utils: 例外', false, 'no error', e.message); }
```
### 3. テスト仕様の詳細
開発仕様書 `docs/dev/dev_mas-196_repo_contracts_test.md` のテストケーステーブルに全テストケースの入力値・期待値が記載されている。実装時に参照すること。
## 制約
- テストで書き込み操作(save/append/setValue 等)は絶対に行わない
- 既存テスト(T1〜T8)のコードは変更しない
- 各テスト関数内でも主要な検証ブロックを try-catch で保護し、1件の失敗が後続を巻き込まないようにする
拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---|
| ファイル読み込み・構造把握 | なし | 機械的な読み取り |
| T9 Contracts テスト実装 | なし | 純粋関数の入出力テスト。ロジック単純 |
| T10 Repository テスト実装 | あり | findAsMap() のキャッシュ・有効フラグ除外の検証ロジック構築に深い推論が有効 |
| T11 Utils テスト実装 | あり | adjustToBusinessDay の月末クランプ+祝日判定の期待値算出に深い推論が有効 |
| runAllTests() 統合 | なし | 既存パターンの踏襲 |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|
| 仕様書作成(本ドキュメント) | Claude Opus 4.6 | 複数ファイルの横断的な構造理解、テストケース設計の網羅性判断、既存テンプレートとの整合性確保に高い推論力が必要 |
| 実装(テストコード追記) | Claude Sonnet 4.6 | 仕様書で入力値・期待値が明確に定義済み。コード生成は仕様に沿った機械的作業が中心のため、速度とコスト効率を優先 |
| 実装レビュー | Claude Opus 4.6 | 書き込み禁止制約の遵守確認、エッジケースの見落としチェックに高い推論力が有効 |
変更履歴