概要

項目内容
案件IDMAS-196
カテゴリテスト
PhaseP1
優先度★★
所要時間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-01toDto: 基本変換Contracts.toDtoheaders=['A','B','C'], row=[1,'x',true] を変換{A:1, B:'x', C:true}
T9-02toRow: 基本変換Contracts.toRowheaders=['A','B','C'], dto={A:1, B:'x', C:true} を変換[1,'x',true]
T9-03ラウンドトリップtoDtotoRowheaders と row から toDto → toRow と往復変換元の row と一致(JSON.stringify 比較)
T9-04toDto: 空ヘッダーキーのスキップContracts.toDtoheaders=['A','','C'], row=[1,2,3] を変換{A:1, C:3}(空キー '' のプロパティが存在しない)
T9-05toRow: DTO に存在しないヘッダーContracts.toRowheaders=['A','B','C'], dto={A:1} を変換[1,'',''](未定義キーは空文字)
T9-06toDtoList: 複数行変換Contracts.toDtoListdata=[['A','B'],[1,2],[3,4]](ヘッダー+2行)を変換headers=['A','B'], rows の長さ=2, 各行が正しい DTO
T9-07toDtoList: ヘッダーのみ(データ0行)Contracts.toDtoListdata=[['A','B']] を変換headers=['A','B'], rows の長さ=0
T9-08toRows: 複数DTO一括変換Contracts.toRows2件のDTOを一括変換各行が toRow 単体と同じ結果

テストスイート T10: Repository 層(読み取り専用)

#テスト名対象関数手順期待結果
T10-01OrderRepository.findAll: 戻り値構造OrderRepository.findAllfindAll() を呼び出し戻り値に headers(配列) と dtos(配列) が存在
T10-02OrderRepository.findAll: ヘッダーに必須列OrderRepository.findAllheaders に '発注ID(ORD)' が含まれるかindexOf !== -1
T10-03InvoiceRepository.findAll: DTO プロパティ存在InvoiceRepository.findAll先頭DTOに '請求ID(INV)', '科目名', '収支区分' が存在するか全て hasOwnProperty === true
T10-04BankTxRepository.findAll: 戻り値構造BankTxRepository.findAllfindAll() の戻り値を検証headersdtos が配列
T10-05JournalRepository.findAll: 戻り値構造JournalRepository.findAllfindAll() の戻り値を検証headersdtos が配列
T10-06AccountRepository.findAsMap: マップ構造AccountRepository.findAsMapfindAsMap() の戻り値を検証オブジェクト型、各値に stmtcat プロパティが存在
T10-07AccountRepository.findAsMap: キャッシュ有効性AccountRepository.findAsMapresetCache() → findAsMap() を2回呼び出し2回目の結果が1回目と同一(JSON.stringify 比較)
T10-08AccountRepository.findAsMap: 有効フラグ=FALSE 除外AccountRepository.findAsMapfindAll() で有効フラグ=FALSE のレコードの科目名を取得し、findAsMap() にそのキーが無いことを確認マップにキーが存在しない

テストスイート T11: Utils 層

#テスト名対象関数手順期待結果
T11-01parseDateToYm: Date型Utils.parseDateToYmnew Date(2025, 0, 15) を変換'2025-01'
T11-02parseDateToYm: スラッシュ形式Utils.parseDateToYm'2025/03' を変換'2025-03'
T11-03parseDateToYm: 和暦風形式Utils.parseDateToYm'2025年3月' を変換'2025-03'
T11-04parseDateToYm: 空値Utils.parseDateToYmnull, '', undefined を変換全て ''
T11-05parseDateToYmd: 日部分なしフォールバックUtils.parseDateToYmd'2025-03' を変換'2025-03-01'(日部分に -01 を付与)
T11-06addMonths: 年跨ぎ加算Utils.addMonths'2025-11' に +3'2026-02'
T11-07addMonths: 年跨ぎ減算Utils.addMonths'2026-02' に -3'2025-11'
T11-08parseAmt: 全角数字Utils.parseAmt'12,345' を変換12345
T11-09parseAmt: カンマ区切りUtils.parseAmt'1,234,567' を変換1234567
T11-10parseAmt: 負数・記号混在Utils.parseAmt'▲-500円' を変換-500
T11-11parseAmt: 数値型パススルーUtils.parseAmt42 を変換42(そのまま返却)
T11-12adjustToBusinessDay: 月末クランプ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-08Contracts 層
testT10_Repository_()T10-01〜T10-08Repository 層
testT11_Utils_()T11-01〜T11-12Utils 層

テストの独立性

  • 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));

注意事項

  1. 書き込み禁止: Repository 層の save() / append() はテストしない。本番データ破損リスクがあるため、読み取り (findAll / findAsMap) のみ検証する
  2. dev 環境限定: T10(Repository 層)は実シートを読み取るため、dev 環境でのみ実行すること。prod 環境ではシートが異なりテスト結果が変わる可能性がある
  3. キャッシュのリセット: T10 で AccountRepository.findAsMap() をテストする前後に AccountRepository.resetCache() を必ず呼び出し、他テストへの副作用を防ぐ
  4. 固定値検証の回避: T10 では環境差異でテストが落ちないよう、特定のデータ値(科目名や金額)ではなく、型・プロパティ存在・配列長 > 0 などのスキーマ検証を行う
  5. prod デプロイ時の自動削除: 900_test/ ディレクトリは prod デプロイ時に GitHub Actions で自動削除されるため、テストコードが本番に混入するリスクはない

関連ドキュメント

仕様書関連箇所
B.1 テスト概要・戦略テストランナーの設計方針、テストケース一覧(T1〜T8 に T9〜T11 を追加)
B.3 統合テスト手順統合テストの実行手順
D.2 MAS-098 RPA冪等性テストT9 ID の予約に関する競合(T7 で実質カバー済み)
CLAUDE.mdテストランナーの配置規約(900_test/901_test_runner.js

人間が検討すべき事項

  • 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書き込み禁止制約の遵守確認、エッジケースの見落としチェックに高い推論力が有効

変更履歴

日付変更内容
2026-04-14初版作成