Mockito
は、Dartでモック、スタブ、およびスパイを簡単に作成できるライブラリです。Mockitoには、テストに使用される様々なMatcherが用意されていますが、既存のMatcherだとうまく行かないことがあったので、自作する方法を調べて実際に実装してみました。
Matcherとは何にか?
Matcherは、テスト中に値を比較するために使用されるオブジェクトです。Matcherを使用すると、テストの読みやすさと保守性が向上します。Matcherは、単純な値と複雑なオブジェクトを比較するために使用できます。
Matcherの使用方法
説明のためにPersonRepository
クラスとPersonRepository
クラスを定義します。
class PersonRepository {
void save(String name, int age) {
// DBなどの永続化する処理
};
}
class PersonService {
final PersonRepository personRepository;
RegistrationViewModel(this.personRepository);
void register(String name, int age) {
// 名前や年齢のバリデーション
personRepository.save(name, age);
}
}
PersonRepository
はDBやローカルに保存する役割を持ち、save
メソッドはDBやローカルストレージなどに名前と年齢の情報を保存する役割を持ちます。PersonService
のregister
メソッドは何らかのボタンなどをタップされた時に使われるイメージで、正しい値が入力されたことの確認をし、保存の処理を呼び出す役割です。
ここで「指定された名前と年齢で保存処理を呼び出すこと」という検証のテストを考えます。
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'person_service_test.mocks.dart';
@GenerateMocks([PersonRepository])
void main() {
late PersonService sut;
late MockPersonRepository personRepository;
setUp(() {
costRepository = MockCostRepository();
sut = RegistrationViewModel(costRepository);
});
test('registerは指定された名前と年齢で保存処理を呼び出すこと', () {
var name = 'Taro';
var age = 21;
sut.register(name, age);
verify(personRepository.save(argThat('Taro', 21)).called(1);
});
}
上記のようにargThat
を用いて指定された名前と年齢で保存処理を呼び出すことを検証できます。
Matcherを自作する
同姓同名で同じ年齢の人がいた時に区別できるよう、Person
クラスを作成し、ユニークなIDを持つようにします。それに従いPersonRepository
の引数とPersonService
の実装を変更します。
import 'package:uuid/uuid.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'person.freezed.dart';
@freezed
class Person {
factory Person._({
required String id,
required String name,
required int age
}) = _Person;
factory Person.of(String name, int age) {
return Person._(
id: const Uuid().v4(),
name: name,
age: age
);
}
}
class PersonRepository {
void save(Person person) {
// DBなどの永続化する処理
};
}
class PersonService {
final PersonRepository personRepository;
RegistrationViewModel(this.personRepository);
void register(String name, int age) {
personRepository.save(Person.of(name, age));
}
}
先ほどと同様に「指定された名前と年齢で保存処理を呼び出すこと」という検証のテストを考えます。IDは生成のたびに変化してしまうので以下のように書いても失敗してしまいます。
test('registerは指定された名前と年齢で保存処理を呼び出すこと', () {
var name = 'Taro';
var age = 21;
sut.register(name, age);
var expected = Person.of(name, age);
verify(personRepository.save(argThat(expected)).called(1);
});
そこで、名前と年齢が等しい時にマッチするようなカスタムMatcherを作ります。
class PersonMatcher extends Matcher {
final Person expectedPerson;
PersonMatcher(this.expectedCost);
@override
Description describe(Description description) {
return description.add('Person: ${expectedCost.toString()}');
}
@override
bool matches(item, Map matchState) {
return item is Person &&
item.name == expectedPerson.name &&
item.age == expectedPerson.age;
}
}
カスタムMatcherを作成する際の基本的な考え方は、Matcher
クラスを継承し、matches
メソッドをオーバーライドすることです。このカスタムMacherを使うことで、以下のようにテストを書くことができます。
test('registerは指定された名前と年齢で保存処理を呼び出すこと', () {
var name = 'Taro';
var age = 21;
sut.register(name, age);
var expected = Person.of(name, age);
verify(personRepository.save(argThat(PersonMatcher(expected))).called(1);
});
まとめ
カスタムMatcherを作成することで、Mockitoを使用してテストをより効果的に実行できます。カスタムMatcherは、テストの可読性と保守性を向上させ、より複雑なオブジェクトの比較も可能にします。
DartのMockitoは元々JavaのMockitoに影響されて作られているので、Java側でできることはある程度できそうな気がします。
最後に
初めて友人と二人でスマホアプリをリリースしました。Spenderというアプリです。
Spender for Android
Spender for iOS
このアプリ開発過程で今回のような学びがありました。また何か学びがあれば書いていきます。
アプリ開発の経緯などはこちらから