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

Null安全は「足枷」ではない。テストにおける「型契約」の真髄

DartのSound Null Safetyは、単なるコンパイラによるチェックではない。それは、「どの値がいつ、どの状態で存在するのか」というプログラムの生存戦略そのものだ。

多くのエンジニアがテストコードを書く際、`null`を許容する型(`Type?`)に直面すると、安易に `!`(強制アンラップ)を使ったり、無意味なダミーオブジェクトで埋め尽くしたりする。これはDart VMの型安全性を自らドブに捨てる行為であり、将来のバグへの招待状だ。

今日は、プロダクションコードの整合性をテストで担保するための「Null許容型とのスマートな付き合い方」を伝授する。

—

1. モックの陥穽:`null` を「許容」するか「排除」するか

テストダブル(モック/スタブ)を作成する際、もっとも避けるべきは「とりあえず `null` を返しておけばいい」という思考停止だ。

`mocktail` や `mockito` を使う際、`anyNamed` や `captureAny` の型定義に無頓着だと、本来 `null` を受け取るべきではないロジックが `null` を受け取り、実行時に `NoSuchMethodError` や `TypeError` を引き起こす。

悪い例:思考停止のスタブ

// 良くないパターン:型安全を無視したスタブ
when(mockRepository.fetchUser(any)).thenAnswer((_) async => null);

これでは、呼び出し側の `User user = await repo.fetchUser(…);` で、DartのSound Null Safetyが静的に弾くはずの「null可能性」が、実行時に爆発する。

—

2. 実践的設計: Null許容型を型システムに組み込む

テスト対象のメソッドが `User?` を返す場合、テスト側では「ユーザーが存在する場合」と「存在しない場合」の2つの世界を、型として峻別しなければならない。

推奨パターン:Provider的アプローチによる型境界の制御

// インターフェース定義
abstract class UserRepository {
Future fetchUser(String id);
}

// テストコードでの洗練されたアプローチ
void main() {
late MockUserRepository mockRepo;

setUp(() => mockRepo = MockUserRepository());

test(‘ユーザーが存在しないケースを型安全に検証する’, () async {
// arrange: 明示的に null を返すことで、呼び出し側の null チェックを強制する
when(() => mockRepo.fetchUser(any())).thenAnswer((_) async => null);

// act
final user = await mockRepo.fetchUser(‘123’);

// assert: ここで型ガードを行う
expect(user, isNull);
});
}

ここで重要なのは、「テストコード自身もプロダクションコードと同じ型推論ルールに従っている」という点だ。`user` は `User?` 型として推論されるため、`user.name` と書けばコンパイラが即座にエラーを吐く。これが「正解」だ。

—

3. パフォーマンスとコンパイル時の最適化

Dart VMにおいて、`null` チェックは極めて高速だ。しかし、不要なモックの多用は `AOTコンパイル` におけるシンボル生成のオーバーヘッドを増大させ、テスト実行時間を確実に削る。

チーフアーキテクトからの助言:

1. `Never` 型の活用: 決して発生してはならないコードパスには `throw UnimplementedError()` ではなく、`Never` 型を返すヘルパー関数を定義せよ。これにより、コンパイラは「その先は到達不能」と断定し、無駄な Null チェックコードを生成対象から除外する。
2. `late` の慎重な使用: `late` はコンパイル時のチェックをランタイムに持ち越す裏口だ。テストクラスの初期化以外では極力避けろ。

—

4. 現場で使える「堅牢なモック」のテンプレート

実務で私が多用している、Null安全性と保守性を両立させるスタブ定義を紹介する。

// プロダクションコードの制約を満たすためのファクトリ
class StubBuilder {
static void setupSuccess(MockUserRepository repo, User user) {
when(() => repo.fetchUser(any())).thenAnswer((_) async => user);
}

static void setupNotFound(MockUserRepository repo) {
// nullを返すことを契約として明示
when(() => repo.fetchUser(any())).thenAnswer((_) async => null);
}
}

// テスト側での呼び出し
test(‘ユーザーが取得できた場合の挙動’, () {
StubBuilder.setupSuccess(mockRepo, User(id: ‘1’, name: ‘Dart Dev’));
// … テスト実行
});

このように「状態のセットアップ」を抽象化することで、`null` 許容型のハンドリングを各テストケースに分散させず、一箇所に集約できる。これがコードの保守性を高め、将来的なAPI変更にも強い設計となる。

—

結論:型安全は「対話」である

DartのNull安全は、開発者がコンパイラと行う「この変数は絶対に空ではないよね?」「ああ、ここは空の可能性があるからケアするよ」という対話だ。

テストコードで `null` を適当に扱うことは、その対話を放棄することに他ならない。型システムを味方につけ、`null` の海を泳ぎ切る。それこそが、伝説的なDartエンジニアへの第一歩だ。

コードは嘘をつかない。君が書いた型定義とテストの隙間から、必ずバグは侵入する。だからこそ、型という名の防御壁を、徹底的に強固に築き上げろ。

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