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

境界を制御せよ: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許容型が介在すると、Dart VMは「型チェックのためのブリッジコード」を生成せざるを得ない。

高頻度で呼ばれるモックにおいて、型が不確定であることはメモリレイアウトの最適化を阻害する。テストの実行速度が遅いと感じる場合、それは往々にして「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()` を極めよ: `any()`に頼らず、`argThat`を用いて型を明示的に絞り込むことで、Dart VM上の型推論エンジンは不要なキャストを機械語レベルで削除する。

DartのSound Null Safetyは、開発者の「怠慢」を許さない冷徹な鏡だ。テストコードにおいてこそ、我々は言語仕様の奥深くまで潜り、コンパイラが生成する機械語の一つ一つに責任を持つべきである。

システムは、書かれたコードの通りにしか動かない。そして、Null安全なコードは、書かれた通りの「境界」を完璧に守り抜く。これこそが、Dartが次世代のエンジニアに授けた最強の武器なのだ。

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