【入門編】Null安全環境での『モックテスト』:Null許容型を考慮したテストダブルの設計戦略 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは。Dartの深淵へようこそ。
FlutterやDart VMの内部構造を追い続けていると、多くの開発者が「Null安全(Sound Null Safety)」という強力な武器を、単なる「エラーを避けるための制約」と捉えてしまっていることに気づきます。

しかし、DartのNull安全は、コンパイラが「実行時にNull参照例外が発生しないこと」を数学的に証明するための仕組みです。テストコードにおいても、この「証明」を崩さない設計こそが、堅牢なアプリケーションを支える鍵となります。

今回は、テストダブル(モック)におけるNull安全との付き合い方について、一歩踏み込んだ話をしましょう。

—

なぜモックでNull安全が「壁」になるのか?

DartのNull安全は、「型システムがNullを含むかどうか」をコンパイル時に厳格に評価します。

例えば、`String`型の引数を取るメソッドをモックしたいとき、テスト側で安易に「なんでもいいから値を返す」ようにすると、Dart VMは「その返り値は本当に型定義と合致しているのか?」と疑い始めます。モックライブラリ(`mocktail`や`mockito`)は、この型システムの隙間を縫って動くため、誤った設定をすると即座にランタイムエラーや型不整合を引き起こします。

—

陥りやすい罠:`any`の過信

初心者が最もやりがちなミスは、何でも受け入れる`any`を乱用することです。

// 良くない例:型安全を無視したモック定義
when(mockService.fetchUser(any)).thenReturn(null); // エラーになる可能性大!

もし`fetchUser`が`User`型(非Null)を返すシグネチャであれば、`null`を返した瞬間に、Dartの型システムは「契約違反」として例外を投げます。これが「Sound Null Safetyの壁」です。

—

戦略的設計:テストダブルにおける「Null許容型」の扱い

テストダブルを設計する際、以下の3つの原則を守るだけで、あなたのテストは劇的に堅牢になります。

1. `any` よりも `anyNamed` や `capture` を選ぶ

`any`は便利ですが、Dartの型推論を攪乱します。可能な限り具体的な型を指定しましょう。

// 推奨されるアプローチ
when(() => mockService.fetchUser(any(named: ‘id’))).thenReturn(userInstance);

2. 「Nullを返す」ことの意味を設計する

もしメソッドが`User?`(Null許容)を返す可能性があるなら、モックもそれを明示的に表現する必要があります。

// User? を返すケースの設計
when(() => mockService.findUser(any())).thenReturn(null); // これは正当な振る舞い

ここで重要なのは、「モックが返す値が、本来の関数の契約(Contract)に沿っているか」という視点です。Nullを返して良いのか、それとも例外を投げるべきなのか。これをテストコードで記述することは、設計の再確認にも繋がります。

—

実践:Mocktailを活用した安全なモック設計

`mocktail`は、Dartの型システムと相性が良く、コード生成が不要なため、現代的なDart開発のスタンダードです。以下のように書くのがベストプラクティスです。

import ‘package:mocktail/mocktail.dart’;

// モッククラスの定義
class MockUserRepository extends Mock implements UserRepository {}

void main() {
final mockRepo = MockUserRepository();

test(‘ユーザー取得が失敗したとき、適切にnullを返すこと’, () {
// コンパイル時に型安全が担保されたスタブ定義
when(() => mockRepo.getUserById(‘123’))
.thenAnswer((_) async => null); // Null許容型への回答を明示

// 実行と検証
final result = mockRepo.getUserById(‘123’);
expect(result, completion(isNull));
});
}

ここがポイント!

  • `thenAnswer`の活用: 単純な`thenReturn`だけでなく、非同期処理を扱う場合は`thenAnswer`でクロージャを渡します。これにより、Dart VM上でのFutureの解決と型チェックがより正確に行われます。
  • 明示的な期待値: `isNull`や`isA()`といったマッチャーを使い、返り値の型を明示的に検証することで、もし将来的に関数の型定義が変わった場合、テストが即座に失敗して「修正すべき場所」を教えてくれます。

—

先輩からのアドバイス:Null安全と戦わない

最後に一つだけ伝えておきたいことがあります。
「テストコードでNull安全を回避しようとしない」でください。

`late`変数や`!`演算子(強制アンラップ)をテストコードで多用しているなら、それは「コードの設計が不安定である」というシグナルです。モック側でNull許容型と戦うのではなく、「どのようなデータが流れてきても型が崩れないインターフェース」を設計することに注力してください。

Null安全は、あなたの敵ではなく、「あなたが書いたコードが正しいことを、コンパイラが裏で保証してくれる」という強力な味方です。

この壁を越えれば、あなたはもうDartの初心者ではありません。型システムを味方につけて、自信を持ってコードを書いていきましょう。何かあればいつでも聞いてくださいね。応援しています!

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