境界を制御せよ:Sound Null Safety下におけるテストダブルの「静的保証」と「動的現実」
DartのSound Null Safetyは、単なる「Nullポインタ例外を防ぐためのLinter」ではない。コンパイラが型グラフを静的に解析し、非Null許容型(non-nullable type)に対して、メモリ上のスタック/ヒープ領域において「Null値が物理的に存在し得ない」ことを保証する、極めて厳格な型の防御線だ。
しかし、テストコードを書く際、我々はこの「理想的な型理論」と「動的なモックの現実」の狭間で苦しむことになる。本稿では、Dart VMのメモリモデルとコンパイル時の最適化を意識した、洗練されたテストダブルの構築手法を伝授する。
—
1. コンパイラが排除する「曖昧さ」とテストのジレンマ
DartのAOTコンパイラは、`String`型の変数がNullになり得ないと判定すれば、生成される機械語からNullチェックを完全に削除する。これがパフォーマンスの源泉だ。しかし、テスト環境において、外部依存(RepositoryやService)をモック化する際、どうしても「未定義状態」をNullで表現したくなる誘惑に駆られる。
ここで多くの開発者が陥るのが、`late`修飾子の乱用や、不必要な`?`(Null許容型)の付与だ。これらはコードの健全性を損なうだけでなく、コンパイラの型推論能力を低下させ、予期せぬ実行時エラー(`LateInitializationError`)を誘発する。
—
2. モックにおけるNull許容型制御の「正攻法」
モックライブラリ(`mocktail`や`mockito`)を使用する際、安易に`any()`を使ってNullを許容させてはならない。以下のコードを見てほしい。
// 良くない例:型システムを骨抜きにしている
when(mockRepo.fetchUser(any())).thenAnswer((_) => Future.value(null));
このコードの問題は、`fetchUser`が本来返すはずの「Userドメインモデル」ではなく、底なしのNullを返し、呼び出し側のビジネスロジックにNullチェックを強制させてしまう点にある。
解決策:Null Object Pattern と「型安全なスタブ」
テストにおいてモックは「偽物」ではなく「期待されるコントラクトの代理人」であるべきだ。Nullを返すのではなく、「状態を持たない正当なインスタンス」を返すのが、Dartの型理論に最も適合する。
// 推奨されるアプローチ:Null Object Pattern
class MockUserRepository extends Mock implements UserRepository {
// 意図的に空のインスタンスを返すことで、呼び出し側のNullチェックを不要にする
// これにより、コンパイラは非Nullとして最適化を継続できる
static final User emptyUser = User(id: -1, name: ‘Anonymous’);
void setupSuccess() {
when(() => fetchUser(any())).thenAnswer((_) async => emptyUser);
}
}
—
3. 非同期境界とIsolateの観点:`Future`の真実
Dart VMのイベントループにおいて、`Future
高頻度で呼ばれるモックにおいて、型が不確定であることはメモリレイアウトの最適化を阻害する。テストの実行速度が遅いと感じる場合、それは往々にして「Null許容型を多用したことによる、ランタイムの型チェックの肥大化」が原因だ。
実行時の防壁を突破するテスト設計
モックのスタブ定義において、可能な限り`T`を具象型に固定せよ。
// 型の厳密性を担保したスタブの例
when(() => repository.getUser(argThat(isA
.thenAnswer((_) async => User(id: 1, name: ‘Test User’));
// もし「失敗」をシミュレートするなら、Nullを返すのではなく例外を投げる
when(() => repository.getUser(‘unknown’))
.thenThrow(UserNotFoundException());
`null`を返して状態を分岐させるのではなく、`Exception`を投げることで、ランタイムの例外処理パス(`catch`ブロック)をテストし、型システムを「非Nullの幸せな経路」に集中させる。これが、真に堅牢なアーキテクチャの知見だ。
—
4. チーフアーキテクトからの提言:コードは「静的」に語れ
テストコードを書く際に意識すべきは、「自分の書いたテストが、コンパイラにどのような最適化を許可しているか」という視点だ。
1. `null` は「値」として扱うな、例外として扱え: モックから`null`を返すのは、設計の敗北である。`Result
2. `late`の多用は「防壁の撤去」である: テストのセットアップでどうしても`late`が必要なら、それはテスト対象のクラスの初期化順序が破綻している証拠だ。
3. `isA
DartのSound Null Safetyは、開発者の「怠慢」を許さない冷徹な鏡だ。テストコードにおいてこそ、我々は言語仕様の奥深くまで潜り、コンパイラが生成する機械語の一つ一つに責任を持つべきである。
システムは、書かれたコードの通りにしか動かない。そして、Null安全なコードは、書かれた通りの「境界」を完璧に守り抜く。これこそが、Dartが次世代のエンジニアに授けた最強の武器なのだ。