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

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` であるなら、テスト側でも必ず `null` を許容するスタブを定義することだ。推論に頼らず型を明示することで、将来的なAPI定義の変更(`String?` が `String` に格上げされる等)が起きた瞬間にテストが失敗し、変更の伝播を即座に検知できる。

—

4. パフォーマンスと設計上の注意点

Dart VMの観点から見ると、Nullチェックは非常に軽量な操作(ポインタ比較と同等)である。しかし、テストコード内での過度な「Null安全性を回避するようなモック」は、実行時のIsolateのオーバーヘッド以上に、開発者の認知的負荷を増大させる。

  • `late` キーワードの使用を避ける: テストのセットアップで `late` を多用すると、初期化忘れによる `LateInitializationError` が頻発する。`setUp` 内で確実にインスタンス化し、Null許容性を保持する設計を優先せよ。
  • 網羅性の追求: 境界値テスト(Boundary Value Analysis)において、Nullは常に重要な「境界」の一つだ。全てのNull許容型に対して、「値が存在する場合」と「Nullの場合」の2パターンを必ず用意する文化をチームに定着させろ。

結論:コードは「書く」ものではなく「守る」もの

Null安全は、開発者が書くコードの「品質の防波堤」だ。モック作成においてNullを適当に扱うことは、その防波堤に自ら穴を開ける行為に等しい。

プロダクションコードの型定義を神聖視し、テストコードでその契約を厳密にシミュレートせよ。それができれば、あなたのDartアプリケーションは実行時エラーから解放され、より本質的なビジネスロジックの構築に集中できるはずだ。

次は、モックを超えた「Fakeパターンによる状態管理」について深掘りするとしよう。議論は尽きない。現場からは以上だ。

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