DartのNull安全を「テスト」で制する:モック戦略の極意
こんにちは。Dartの深淵へようこそ。
Dartを触り始めた皆さんが最初に出会う、そして最も強力な武器となるのが「Sound Null Safety(健全なNull安全)」ですよね。
「コンパイル時にNullによるクラッシュを根絶する」というこの設計は、Dartの型システムが実行時(Runtime)の安全性を保証するために、コンパイル時にどれほど厳格なチェックを行っているかを物語っています。
しかし、いざテストコードを書く段になると、「モックでNull許容型をどう扱うべきか?」という壁にぶつかる方は非常に多いです。今回は、Dartのコンパイラが裏側で何を考えているのかを紐解きながら、テストの現場で迷わないための「Null安全との付き合い方」を伝授します。
—
1. なぜテストで「Null許容型」が問題になるのか?
Dartの型システムは、変数に `?` が付いていない限り「絶対にNullは入らない」と断言します。コンパイラはその前提で最適化を行います。
テストでモックを作成する際、この「健全性」が仇となることがあります。例えば、「APIのレスポンスが空(Null)の場合」をテストしたいのに、モックの型定義がNullを許容していないと、そもそもテストコード自体がコンパイルエラーを起こしてしまいますよね。
ここで無理に `!` (バン演算子)を使って「これはNullじゃない!」と強制的に教え込むのは、Dartの型システムの思想に対する冒涜です。
—
2. 実践:Null許容型を考慮したモックの作り方
例えば、ユーザー情報を取得するサービスをテストする場合を考えてみましょう。
// ユーザーの型定義
class User {
final String name;
final String? bio; // bioはNullの可能性がある
User({required this.name, this.bio});
}
// サービス層の抽象化
abstract class UserRepository {
Future
}
この `UserRepository` をテストするためのモックを作る際、皆さんはどうしていますか?
推奨されるアプローチ:型を正確に表現する
モックライブラリ(`mockito` や `mocktail`)を使う際、戻り値の型を曖昧にしてはいけません。
import ‘package:mocktail/mocktail.dart’;
class MockUserRepository extends Mock implements UserRepository {}
void main() {
final mockRepo = MockUserRepository();
test(‘ユーザーが存在しない場合、nullを返すこと’, () {
// コンパイラに「このモックはnullを返しても良い」と明示する
when(() => mockRepo.fetchUser(any())).thenAnswer((_) async => null);
// テスト実行…
});
}
ここが重要です: `thenAnswer((_) async => null)` と記述することで、Dartの型システムは「ああ、このメソッドは `User?` を返す可能性があるんだな」と正しく認識します。これにより、テスト内の後続のコードで安全に `if (user == null)` といったチェック(型プロモーション)を行うことができるのです。
—
3. 陥りやすい罠:「!」への依存を避ける
初学者が最もやりがちなミスが、テストを通すためだけに `!` を多用することです。
// 悪い例:強制的なアンラップ
final user = await mockRepo.fetchUser(‘1’);
expect(user!.name, ‘Dart-kun’); // もしnullが返ってきたらテストはクラッシュする
この書き方は、「テストコード自体が例外を投げて死ぬ」という最悪の結果を招きます。健全なテストコードは、「Nullであるケース」と「そうでないケース」を型安全に切り分ける必要があります。
解決策:型プロモーションを活用する
Dartのコンパイラは、`if` 文によるチェックを記憶しています。
final user = await mockRepo.fetchUser(‘1’);
if (user == null) {
// ここではuserは確実にnull。null時の振る舞いをテストする
expect(user, isNull);
} else {
// ここではuserは確実にUser型。プロパティに安全にアクセスできる
expect(user.name, isNotNull);
}
このように書くことで、コンパイラは「このスコープの中では `user` はNullではない」と判断します。これがDartのフローベースの型解析です。
—
4. チーフアーキテクトからのアドバイス:テストは「仕様書」である
最後に、一つだけ覚えておいてください。
「テストコード内のNull許容型は、プロダクトコードの仕様を反映する鏡」です。
もしモックで「Nullを返させる」ために苦労しているのであれば、それは「プロダクトコード側でNullを許容しすぎている」という設計のサインかもしれません。DartにおいてNullを許容するということは、その後の処理で必ず「Nullチェック」というコストが発生することを意味します。
- Nullを返すべき場所か?
- デフォルト値(Empty Objectパターン)で回避できないか?
これらをテストを書く過程で問い直すことが、結果として堅牢なアプリケーションを生み出す近道になります。
—
まとめ:ここをクリアすれば大丈夫!
1. モックの型定義をケチらない: Null許容型が必要なら、正直に `?` を付けた型で設計しましょう。
2. `!` を封印する: テストコードこそ、プロダクトコード以上に安全であるべきです。
3. 型プロモーションを信じる: `if (x != null)` を使えば、Dartは賢く型を推論してくれます。
Dartの型システムは、皆さんの敵ではなく、最も信頼できる「コンパイル時のペアプログラマー」です。このルールさえ守れば、あなたの書くテストはどんなに複雑なロジックでも、エラーを未然に防ぐ強力な要塞となるはずです。
さあ、自信を持って次のテストを書いてみてください。きっと今までよりずっとスムーズに進むはずですよ!