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

Sound Null Safety下のテスト戦略:コンパイラの型推論をハックし、ランタイムの整合性を極限まで高める

DartのSound Null Safetyは、単なる「Nullポインタ例外を防ぐための静的解析」ではない。それは、コンパイル時に確定された「型の不変条件(Invariant)」を、AOTコンパイルされたバイナリのメモリレイアウトレベルで保証する、極めて強力な契約だ。

多くのエンジニアは、テストコードにおいて「とりあえず `!` をつける」あるいは「`null` を許容するモックを作る」という安易な妥協を行う。しかし、それはランタイムにおけるType Flow Analysisを汚染し、Dart VMが本来持っているはずの最適化能力を削ぐ行為に他ならない。

本稿では、Null安全環境下でのモック構築を、VMの挙動と型システムの深層から再定義する。

—

1. コンパイラの視点:`?` は「型」ではなく「分岐」である

Dartのコンパイラ(`dart2js` や `dart2native`)は、変数が `T?` であると認識した瞬間、その変数がアクセスされる全ての箇所に、暗黙の Null Check(`CHECK_NULL` 命令)を挿入する。

テストコードで無闇に `T?` を導入することは、単に「テストが通る」だけでなく、「コンパイラが本来排除できたはずの Null チェック命令を、実行バイナリに残存させる」ことを意味する。これはホットパスにおいては微細な、しかし確実に積もるパフォーマンスの劣化を招く。

誤ったモックの構築例

// 典型的なアンチパターン
class MockUser implements User {
@override
String? name; // インターフェースが Nullable でないのに、モックで Nullable にするのは悪手
}

このモックを `required` 引数に渡す際、コンパイラは型昇格(Type Promotion)を阻害され、実行時に余計なガードコードを吐き出す。真のシニアは、「テスト用の型定義」と「プロダクションの型定義」を分離し、コンパイル時に Nullability を完全排除する。

—

2. 厳密なモック構築:`late` と `required` の正しい責務

テストコードにおいて、コンストラクタで初期化できない依存関係に対し、反射的に `?` を使ってはならない。代わりに `late` を活用し、コンパイラの「初期化チェック」をあえてバイパスすることで、実行時の整合性を保証する。

class MockRepository implements Repository {
// コンパイル時は非Nullを保証しつつ、テストのセットアップで注入する
late final User _mockUser;

void setupUser(User user) => _mockUser = user;

@override
User get currentUser {
// ここで敢えてチェックを入れることで、テスト設計の不備をランタイムエラーとして即座に検知する
// 「黙ってNullを返す」のではなく「設計の崩壊をクラッシュで通知する」のがSoundなアプローチ
return _mockUser;
}
}

このアプローチの利点は、イベントループのキュー消費メカニズムに直結する。非同期テストにおいて、誤った Nullable なモックは `Future` の解決を待たずにNullを伝播させ、デバッグが困難な `TypeError` を発生させる。`late` を使用すれば、初期化漏れは即座に `LateInitializationError` として捕捉でき、スタックトレースが極めて明瞭になる。

—

3. 型の防壁を突破する:`Never` と `dynamic` の使いどころ

モック作成において、意図的に「型の破壊」を行わなければならないケース(例えば、プライベートな内部状態のテストなど)では、`dynamic` へのキャストではなく、`Never` 型を返すべき場面がある。

// 意図的に未実装箇所を叩かれた場合、実行を停止させる
Never fail(String message) => throw StateError(message);

class ForbiddenMock implements SecureService {
@override
String get secretKey => fail(“Security Breach: 秘密鍵にアクセスしてはならない”);
}

この手法は、テストの網羅性を高める。`return null;` と記述すると、呼び出し元は `null` を処理するコードパスを通らざるを得ない。しかし `throw` を通せば、「そのコードパスが本来到達不可能であること」をテスト実行時にVMレベルで証明できる。

—

4. チーフアーキテクトからの提言:Null安全は「テストの質」を写す鏡である

Null安全なコードベースにおいて、テストが何度も `null` に悩まされるのは、そのアーキテクチャ自体が Null を必要悪として抱え込んでいる証拠だ。

1. モックには Nullable を持ち込まない: 依存関係を `late` で管理し、初期化の責務を明示せよ。
2. デフォルト値は「意味のある値」にせよ: `””` や `0` が許容されるなら、それは Nullable ではなく、その型を定義すべきだ。
3. VMの最適化を信じろ: 型システムに正しく従えば、コンパイラは `null` チェックを `LTO (Link Time Optimization)` で完全にインライン展開し、物理メモリへのアクセスを最小化してくれる。

Dartの Null Safety は、単なる言語仕様ではない。それは、君たちのコードをより厳格に、より高速に、そしてより予測可能な状態へと導くための、コンパイラとの「契約」なのだ。

テストコードにおいて `?` をタイプする前に、一度立ち止まって考えてほしい。その Null は、本当に必要なのか? それとも、君の設計が「Null を許容する怠惰」に甘えているだけではないのか?

妥協なきコードこそが、最高峰のパフォーマンスを生む。

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