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

Dartの「Sound Null Safety」をテストコードで攻略する:モックとスタブの設計術

こんにちは。Dartの深淵を歩む皆さん、今日も型システムと対話していますか?

Dartの最大の特徴であり、同時に多くのエンジニアを悩ませるのが「Sound Null Safety(健全なNull安全)」です。コードを書いている時は「Nullを許容するか否か」がコンパイル時に厳密にチェックされるため非常に安心ですが、テストコードを書く段になると、急に「モックしたオブジェクトがNullを返してしまい、実行時エラー(あるいは型エラー)になる」という壁にぶつかることはありませんか?

今日は、Dartのコンパイラがどのように型を解釈しているかを紐解きながら、テストダブル(モック・スタブ)をスマートに扱うための設計指針を伝授します。ここをクリアすれば、あなたのテストコードは堅牢な要塞になりますよ。

—

1. なぜテストで「Null安全」が壁になるのか?

DartのNull安全は「実行時にNullが混入する可能性をコンパイル時に排除する」という約束事です。しかし、テスト環境においては、本物の依存先を排除するために「空っぽのオブジェクト(モック)」を差し込みます。

ここで問題になるのが、「モックが本来返すはずの型を、Null許容型(Nullable)として定義するか、非Null型として定義するか」という揺らぎです。

例えば、ユーザー情報を取得するクラスを考えてみましょう。

class User {
final String name;
User(this.name);
}

abstract class UserRepository {
User? fetchUser(int id); // IDが見つからない場合、Nullを返す可能性がある
}

このとき、モックライブラリ(`mockito`など)を使ってテストを書く際、`fetchUser`がNullを返すシナリオと、値を返すシナリオの両方を型安全に制御する必要があります。

—

2. モックにおける「Null許容型」のスマートな扱い方

よくある間違いは、すべてを「Null許容型」にしてしまい、後続のコードで `!`(強制アンラップ)を乱用することです。これはDartの型システムの恩恵を自ら捨てているようなもの。

推奨テクニック:`thenReturn` と `thenAnswer` の使い分け

モックを作成する際は、戻り値の「型」を明確に意識してください。

// 正しいモックの定義例
void main() {
final mockRepo = MockUserRepository();

// 1. 値が確実に返るケース(非Null型として扱う)
when(mockRepo.fetchUser(1)).thenReturn(User(“Dart Master”));

// 2. Nullが返るケース(明示的にNullを返す)
when(mockRepo.fetchUser(999)).thenReturn(null);

// これにより、テスト実行時に型不一致による不意のクラッシュを防げます
}

もし、`thenReturn(null)` を書いたときにコンパイラが怒ってくるなら、それはあなたのインターフェース設計で「戻り値が非Null型である」と定義されているのに、テスト側でNullを返そうとしているからです。テストは「仕様の鏡」です。 型エラーが出るのは、実装とテストの間に仕様の齟齬がある証拠なのですよ。

—

3. 「late」や「?」で逃げない:スタブの設計思想

テストコードでよく見かけるのが、フィールド定義での `late` の乱用です。

// 悪い例:lateで無理やり初期化を遅延させる
late MockUserRepository repository;

`late` は非常に強力ですが、初期化を忘れると実行時に `LateInitializationError` が発生します。テストのセットアップで `setUp()` を使う際、「Nullable型」として定義し、テストごとに初期化するのが最も安全なアプローチです。

class MyTest {
UserRepository? _repository; // Nullableにしておく

// ゲッターを用意して、使う場所で安全にアクセスする
UserRepository get repository => _repository!;

@setUp
void setup() {
_repository = MockUserRepository();
}
}

このように「テスト実行前には必ずインスタンスが存在する」という状態をコードで担保することで、`!` を使う箇所を最小限(かつ安全な場所)に限定できます。

—

4. 陥りやすい罠:`any` マッチャーと型安全

`mockito` を使う際、`any` を多用していませんか?

// 危険な例
when(mockRepo.fetchUser(any)).thenReturn(User(“Test”));

これだと、引数に何が入っても `User` を返してしまいます。もし `fetchUser` が `int` を引数に取るなら、`anyNamed` や `argThat` を使って型を絞るのが、熟練者の作法です。

// より厳密なスタブ定義
when(mockRepo.fetchUser(argThat(isA())))
.thenReturn(User(“Strict User”));

`isA()` を使うことで、Dartのランタイム型検査をテストに組み込めます。型が合わない値が渡された瞬間にテストが失敗するため、バグの混入にいち早く気づくことができます。

—

まとめ:型は「守り」ではなく「武器」である

DartのNull安全は、一見すると開発を窮屈にする制約のように感じられるかもしれません。しかし、テストコードにおいて型を厳密に扱うことは、「どの値がNullになり得るか」という仕様をコード自体にドキュメントとして刻み込む行為なのです。

1. `thenReturn(null)` を恐れない:Nullは仕様の一部です。
2. `late` よりは `?` + ゲッター:安全にアクセスする口を絞りましょう。
3. `isA()` で型を縛る:マッチャーを賢く使って、テストの精度を上げてください。

この感覚を身につければ、あなたの書くDartコードは、実行時エラーから解放された極めて頑強なものになるはずです。さあ、次はどんな複雑な依存関係をテストしてみますか? 応援していますよ!

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