Null安全は「制約」ではなく「コンパイラとの契約」である
DartのSound Null Safetyは、単なるNullチェックの自動化ではない。Dart VMのAOTコンパイル過程において、「この型は絶対にNullにならない」という数学的な証明をコンパイラに提供する仕組みだ。
我々がテストコードを書くとき、Null許容型(`T?`)をモックするのは退屈な作業ではない。それは、プロダクションコードの型安全性という「契約」をテストという戦場で再検証する、極めて重要な儀式である。
今回は、Null安全環境下でのモック作成における「アンチパターン」を排除し、コンパイラを味方につけるための設計戦略を伝授する。
—
1. なぜ「雑なNull許容」がテストを殺すのか
多くの開発者が陥る罠は、テストのセットアップで `null` を安易に許容することだ。
// 悪い例:Null許容型を適当にnullで埋める
final mockUser = MockUser(id: null, name: null);
これを行うと、テスト対象のコード(SUT: System Under Test)内で `user.id!` のような強制アンラップ(`!`)が多発する。これは「設計の不備」を「演算子による強行突破」で隠蔽しているに過ぎない。テストコードで `!` を使った時点で、そのテストはプロダクションコードのNull安全性を検証できていない。
—
2. 実践:Null許容型を考慮したモックパターン
Null許容型を扱う際のベストプラクティスは、「Nullであるケース」と「値があるケース」を分離したFactoryを用意することだ。
推奨設計:テスト専用のファクトリメソッド
// プロダクションコード
class User {
final String id;
final String? email; // Null許容型
User({required this.id, this.email});
}
// テストコード
extension UserTestHelper on User {
/// Null許容型のカバレッジを網羅するためのヘルパー
static User create({
String id = ‘default_id’,
String? email = ‘test@example.com’, // デフォルト値を明示
}) => User(id: id, email: email);
}
void main() {
test(‘Emailがnullの場合でもクラッシュしないこと’, () {
// 明示的にnullを渡すテストケース
final user = UserTestHelper.create(email: null);
expect(user.email, isNull);
// 実行時にnullチェックを強制する(is演算子やif-null-checkを使用)
final display = user.email ?? ‘No Email’;
expect(display, ‘No Email’);
});
}
この設計の利点
1. コンパイル時の静的解析: `!` 演算子を一切使わずにテストが完結する。
2. 保守性: `User` クラスのコンストラクタが将来変更されても、修正箇所は `UserTestHelper` の1箇所で済む。
3. 明示的な意図: `email: null` を渡すことで、「このテストはNull許容パスを検証している」という意思表示が明確になる。
—
3. 非同期APIモックと「Nullの境界線」
非同期処理におけるNull許容は、APIレスポンスの欠落を意味する。ここで重要なのは、`Future
堅牢なモック構築パターン
import ‘package:mocktail/mocktail.dart’;
class MockApiClient extends Mock implements ApiClient {}
void main() {
late MockApiClient mockClient;
setUp(() {
mockClient = MockApiClient();
});
test(‘APIがnullを返した場合のハンドリング’, () async {
// Whenの戻り値で明確にnullを許容する
when(() => mockClient.fetchUserEmail(any())).thenAnswer((_) async => null);
final result = await mockClient.fetchUserEmail(‘123’);
// 実行結果がnullであることを型安全に検証
expect(result, isNull);
});
}
ここで重要なのは、`mocktail` や `mockito` を使う際、戻り値が `Future
—
4. パフォーマンスと設計上の注意点
Dart VMの観点から見ると、Nullチェックは非常に軽量な操作(ポインタ比較と同等)である。しかし、テストコード内での過度な「Null安全性を回避するようなモック」は、実行時のIsolateのオーバーヘッド以上に、開発者の認知的負荷を増大させる。
- `late` キーワードの使用を避ける: テストのセットアップで `late` を多用すると、初期化忘れによる `LateInitializationError` が頻発する。`setUp` 内で確実にインスタンス化し、Null許容性を保持する設計を優先せよ。
- 網羅性の追求: 境界値テスト(Boundary Value Analysis)において、Nullは常に重要な「境界」の一つだ。全てのNull許容型に対して、「値が存在する場合」と「Nullの場合」の2パターンを必ず用意する文化をチームに定着させろ。
結論:コードは「書く」ものではなく「守る」もの
Null安全は、開発者が書くコードの「品質の防波堤」だ。モック作成においてNullを適当に扱うことは、その防波堤に自ら穴を開ける行為に等しい。
プロダクションコードの型定義を神聖視し、テストコードでその契約を厳密にシミュレートせよ。それができれば、あなたのDartアプリケーションは実行時エラーから解放され、より本質的なビジネスロジックの構築に集中できるはずだ。
次は、モックを超えた「Fakeパターンによる状態管理」について深掘りするとしよう。議論は尽きない。現場からは以上だ。