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

Sound Null Safetyを「テストの障害」と捉えるな:モック設計の極意

DartのSound Null Safetyは、コンパイル時にメモリレイアウトの不整合を排除する強力な静的解析機構だ。しかし、多くの開発者がテストコードを書く際に、Null許容型(`T?`)の扱いに苦慮し、`!`(Bang演算子)を乱用したり、無理やり `null` を詰め込んでテストを壊している。

断言しよう。テストコードにおける `!` は「設計の敗北」のサインだ。

今回は、Dartのコンパイラがどのように型を推論し、Isolateの境界を越えて型安全性を維持しているのかを理解した上で、実務で絶対にバグを生まない「堅牢なモック設計」の極意を伝授する。

—

1. なぜ「!`」や「late」に頼るのが危険なのか

テストコードで `mock.result!` と書くとき、君たちはコンパイラに対して「ここは絶対にnullではないと保証するから、チェックをサボれ」という命令を下している。これは、プロダクションコードでNull安全を導入した意味を完全に無効化する行為だ。

もしプロダクションコードのAPI仕様が変更され、`null` を返すようになった瞬間、テストはコンパイルをパスしつつ、実行時に `NullThrownError` を吐いてクラッシュする。これでは「テストが信頼できない」状態に陥る。

—

2. 賢明なモック設計:`null` を「許容」するのではなく「明示」する

モックライブラリ(`mocktail` や `mockito`)を使用する際、戻り値に `null` が含まれるケースをどう扱うか。ここで重要なのは、「テスト対象がNull許容型をどうハンドリングしているか」をモックがシミュレートできているかだ。

実践的なモックパターン:NonNullとNullableの境界分離

例えば、ユーザーのプロフィールを取得するサービスクラスのテストを考えよう。

// プロダクションコード
class UserRepository {
Future getDisplayName(String userId) async { / …API通信… / }
}

// テストコード:美しいモック設計
void main() {
final mockRepo = MockUserRepository();

test(‘DisplayNameがnullの場合、デフォルト値を返すこと’, () async {
// 1. Arrange: Nullが返るパターンを明示する
when(() => mockRepo.getDisplayName(any())).thenAnswer((_) async => null);

final service = UserService(mockRepo);
final result = await service.getEffectiveName(‘user_123’);

// 2. Assert: !を使わず、期待値を検証する
expect(result, equals(‘ゲストユーザー’));
});
}

ここでのポイント:

  • `when(…)` の戻り値として `null` を明示的に許容する。
  • テスト対象のメソッド(`getEffectiveName`)が、`null` が返ってきた際にどう振る舞うべきか、という「ビジネスロジック」を検証する。
  • テストコード内で型キャストを一切行っていない点が、堅牢性の証明だ。

—

3. パフォーマンスとコンパイル時評価の視点

Dart VMにおいて、`T?` は内部的に型タグとユニオン的な管理が行われる。Null安全が有効な環境では、コンパイラは `null` チェックを最適化し、ブランチ予測の効率を最大化する。

もしテストコードで `late` 変数を多用し、初期化をテスト実行時に先延ばしにすると、初期化のオーバーヘッドが発生するだけでなく、ライフサイクルの管理が複雑になる。

推奨される設計:Nullableなモックの注入

// NG: late初期化はテストの初期化ミスを誘発する
// late MockRepo repo;

// OK: Null許容型で持ち、必要に応じて初期化する
MockUserRepository? _repo;
MockUserRepository get repo => _repo ??= MockUserRepository();

// tearDownで明示的に解放することで、Isolate内のメモリリークを防ぐ
tearDown(() => _repo = null);

このパターンを採用することで、テスト間の状態汚染(State Pollution)を防ぎ、ガベージコレクションを適切に機能させることができる。

—

4. 現場で使える「堅牢なモック」のチェックリスト

君たちのチームのコードレビューで、以下の基準を設けてほしい。

1. `!` を使っていないか?:使っているなら、そのテストは「型の不確実性」から目を背けている。
2. `null` は境界値か?:`null` は単なるエラーではなく、「未設定」という一つの状態である。その状態をテストケースとして独立させているか。
3. `void` と `Future` の混同はないか?:非同期テストで `await` を忘れると、Null安全以前に非同期処理の整合性が崩壊する。常に `await` があることを静的解析で保証せよ。

—

結論:型安全性は「信頼の積み重ね」である

Null安全は、単なるプログラミングの制約ではない。「この変数は絶対に `null` ではない」という君たちの意志を、Dart VMに伝えるための契約だ。

テストにおいて `null` を適切に扱うことは、プロダクションコードにおいて発生しうる「想定外のクラッシュ」を未然に防ぐための強力な防波堤となる。`!` を捨て、コンパイラの型推論と仲良くなれば、君たちの書くコードは驚くほどバグが減り、保守性が向上するはずだ。

明日からのレビューでは、`!` を見つけたらこう言ってやってくれ。
「その感嘆符は、設計をサボった君自身の悲鳴に聞こえるよ」と。

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