【テクニカル・上級編】Null安全環境での「テストコード」:Null許容型を考慮したモックとスタブの設計 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

Null Safetyの「その先」:コンパイラが許容する脆弱性をテストコードで封殺する技術

DartのSound Null Safety(SNS)は、単なる静的解析の気休めではない。コンパイラ(`dart2js`や`dart2wasm`、あるいはAOTコンパイラ)が生成する機械語において、nullポインタ参照の可能性を「型システムによって論理的に排除した」という宣言だ。

しかし、テストコードを書く際、多くのエンジニアは「`null`を許容するモック」を安易に定義し、せっかくの静的保証を台無しにしている。本稿では、Dart VMのメモリレイアウトと型システムの厳密性を理解した上で、いかにして「堅牢なテスト」を構築するかを説く。

—

1. コンパイラが「型」を認識する瞬間と、テストの陥穽

DartのSNSは、`null`が代入される可能性のある箇所に、コンパイラが実行時チェック(ガード)を挿入することで成立している。しかし、テストコードにおいてモックライブラリ(`mockito`など)が生成する動的な型定義は、時としてこのガードを欺く。

例えば、`late`修飾子を多用したモック設計は、メモリ上の領域を「未初期化(uninitialized)」のまま放置するリスクを孕む。これは、Dart VMのメモリ管理において、ヒープ上の特定のポインタが指し示す先が「実在しない」状態を許容してしまうことと同義だ。

不適切なモックの例(アンチパターン)

// メモリ空間上で null を許容しすぎて、型安全性の防壁を弱めている例
class MockRepository extends Mock implements UserRepository {
// 戻り値が null になることを許容し、呼び出し元に例外を強いる設計
@override
User? getUser(int id) => null;
}

この設計は、`getUser`を呼ぶ側に「毎回`null`チェックを強制する」というコストを支払わせる。本来、プロダクションコードでは「データが存在する」ことが保証されているドメイン層であっても、テストが甘いせいで呼び出し側のコードに不必要な`?`や`!`が散乱し、コードベース全体の汚染を招く。

—

2. 厳密なスタブ戦略:OptionパターンとNonNullなモック

テストにおいて`null`を返すことは、原則として「期待される振る舞いの欠如」である。これを回避するためには、モックを「Null許容型を返すもの」として定義するのではなく、「想定される有効な値を内包するオブジェクト」を返す設計へ昇華させる必要がある。

推奨されるアプローチ:デフォルト値と不変性の保持

// コンパイラが常に非nullを推論できるように設計されたモック
class RobustMockRepository extends Mock implements UserRepository {
@override
User getUser(int id) {
// 戻り値を null にせず、必ずデフォルトの状態を持つインスタンスを返す
// これにより、呼び出し側のコードは null チェックの呪縛から解放される
return User.anonymous(id: id);
}
}

この設計の肝は、「テスト用の特殊なインスタンス」をドメインモデルの一部として定義することだ。これにより、AOTコンパイラは`getUser`の戻り値を型保証された領域として最適化でき、CPUレベルでの分岐予測効率も向上する。

—

3. IsolateとNull Safetyの境界線

Dart VMにおけるIsolateは、メモリを共有しない。そのため、テストコードがモックを介して渡すオブジェクトは、Isolate間でのメッセージ受け渡しが発生する際に「正当な型」としてシリアライズ・デシリアライズされなければならない。

ここでNull許容型が混じると、デシリアライズ時に予期せぬ型変換エラー(`TypeError`)を引き起こす。特に、非同期テスト(`async`/`await`)において、イベントループのキューが消化されるタイミングで、スタブが解決した値が`null`であると、VMのランタイムは即座にガードを突きつけ、Isolateを強制終了させる。

テストにおける同期的な型保証の強制

test(‘非同期データのフェッチにおける型安全性の担保’, () async {
final repo = RobustMockRepository();

// イベントループのキューが空になるまで待機
final user = await repo.getUser(1);

// コンパイラに「userは決してnullではない」と明示する(アサーションによる補強)
expect(user, isA(), reason: ‘Repository must never return null’);
expect(user.id, 1);
});

—

4. 結論:コードの重みを知る者だけが書けるテスト

DartのNull Safetyは、単なる構文上の制限ではない。それは、コンパイラに対して「どのメモリ領域が安全か」を教えるための契約だ。

  • Nullを許容するな: テストのためのモックであっても、プロダクションコードと同じ型制約を課せ。
  • デフォルトインスタンスを作れ: `null`を返す代わりに、`Empty`や`Unknown`という状態を持つオブジェクトを定義せよ。
  • ランタイムの挙動を信じろ: 型安全性を損なうテストコードは、いずれIsolateのイベントループ内で致命的な例外を吐く。

我々エンジニアが書くテストコードは、プロダクションコードの守護者であるべきだ。型安全性をテストのために投げ捨てるなど、あってはならない。コンパイラを味方につけ、型という名の最強の防壁を構築せよ。

それが、Dartという言語を真に掌握するということだ。

タイトルとURLをコピーしました