Mockitoでスタブとモックについてのまとめです. 実際の使い方というよりはMockとStubが必要なのは何故か, そしてどんな場合かといことを簡単に書き留めておきたいと思います. 使い方はMockitoパッケージのページを見ればだいたい分かると思います.
コードが複雑化して, 他のオブジェクトを依存性として持ち処理を委譲するようになると, 単体テストのように値の妥当性を検証するだけでは品質を保証できなくなる. 依存性がきちんと呼び出されていることを検証し(verify), その結果が得られた値が期待通りのものであることが求められるから(expect). 前者の検証ではメソッドが呼び出されたことを検証できる機能が必要でその機能を提供するのがモックである.
テストで使われる偽物オブジェクトの総称をテスト・ダブルと呼びます. Martin Fowler氏のMocks Aren't Stubsでは以下5つが紹介されている.
テスト・ダブル | 説明 |
---|---|
Dummy | 何も実装しない |
Fake | 本物の簡易版 |
Stubs | 決まった値を返す機能 |
Spies | 実際のクラスのメソッドの呼び出しを諜報する |
Mocks | メソッドの呼び出しを記録する |
これらは元々はMeszarosという人が定義したらしいです. 元々の定義ではDummyはテスト・ダブルではないと主張されているそうです.
それぞれの役割は二つに分けられるそうです.
MockだけがBehavior verificationのためのテスト・ダブルだそうですが, スパイとの違いがよく分かりません. FlutterのMockitoの元になったJavaのMockitoのSpyの使い方を見ると, 実際のオブジェクトのラッパーとして使うようです. 対象となるクラスとの交信(メソッド呼び出し)を傍受する(spy)と考えると理解しやすいかもしれません.
A Closer Look at Test Spies
Test Doubles — Fakes, Mocks and Stubs.
Mockitoのドキュメントを見るととりあえずStubとMockの違いがわかっていれば良さそうです(Fakeもありますが割愛します). 両者はテスト対象が状態(値)か振る舞いかで分類できました.
スタブでは値とメソッドの呼び出しを対応づけします. あるメソッドを呼び出したら, 決まった値を返すというルールを決めるわけです.
一方Mockはメソッドが正しく呼び出されたか検証します. スタブだけだと, 期待する結果が得られる過程を検証できません. 正しい順番でメソッドが呼び出され(あるいは呼び出されなかった)結果として期待する値が得られたことを確かめる必要があります.
Mockはテスト対象(System Under Test)が実行される際に必要な別のオブジェクトの偽物です. いわゆる依存性というやつです. Mockitoが提供するMockクラスを拡張しモックとしてAPIが必要な対象をimplementsします.
class MockClass extends Mock implements WanToBeMock {}
final mock = MockClass();
final sud = SystemUnderTest(mock);
SystemUnderTestクラスがテスト対象となるクラスです. このクラスの依存性としてモックが渡されているというのがポイントです. Mock化したクラスには以下の特徴があります.
モック・インスタンスは仕様通りメソッドを呼び出せるものの何もしません. つまり値を返せないわけです. ただ値の検証はテストの大事な要素なのでスタブで実現します.
メソッド呼び出しが特定の値を返せばスタブで達成したいことを実現できます. whenとthenReturn関数を使います.
when(mock.doSomething()).thenReturn(100);
これでmockのdoSomethingメソッドが呼び出された時100を返すというという条件を明示できました.
class MockClass extends Mock implements WanToBeMock {}
void main() {
SystemUnderTest sud;
MockClass mock;
setUp(() {
mock = MockClass();
sud = SystemUnderTest(mock);
});
test('should return 100 when mock calculates 10 to the power of 2 (10^2).', () {
// Arrange
when(mock.pow(10, 2).thenReturn(100);
// Act
sud.pow(10, 2);
final result = sud.getData();
// Assert
verify(
mock.pow(10, 2) // (1)
);
expect(result, equals(100)); // (2)
});
}
SystemUnderTestは様々な計算をラップしたクラスのようです. また実際の処理は別のオブジェクトに委譲しているようです. この場合単に100を返すようなべき乗の計算をSystemUnderTestのpowに実装するだけでは不十分です.
実際の計算処理はmockインスタンスに委譲する必要があります. しないとテストがパスしません. テストにパスするためには, powの結果が100であるだけでは不十分です.
という両方を満たすようにSystemUnderTestを実装する必要があります.
テスト対象に依存性がない場合はわざわざMockitoを使う必要はありません. testパッケージを利用した所謂単体テストで十分です.
An introduction to unit testing
多くの場合全ての処理を自前で書くことはないので, 何らかのライブラリ(自作を含む)が提供するクラスを使う必要があります. それらをモックとして利用することで, 単に値が妥当である以上のことをテストできるようになります.
テストを書けと言われても何を書いていいか分かりにくいです. 特にMockを使うような場合, つまり依存性が存在する場合に指針があると分かりやすいです. またこの一般的な手順が分かっているとテストコードを読む時もまとまりが見えてきます.
AAAはArange, Act, Assertの略でテストを書くときの一般的な手順を表します. Arrangeではwhenによるstubbingなんかを行います. Actではテスト対象を実際に実行します. Asseertではメソッドは正しく呼び出せているかやその結果の戻り値が正しいかを検証します. 頭の中でこのフェーズを意識しておくことでテストコードが書きやすくなると思います.
フェーズ | 説明 |
---|---|
Arrange | テスト・ダブルやインスタンスの準備 |
Act | テスト対象の呼び出し |
Assert | 結果の検証 |
TDD(Test Driven Development)ではテストは最初失敗するように書きます(Red). これは実装がないところからテストを始めるのでActが失敗するのは当然です. その後テストをパスできるように実際のコードを実装します(Green). その後このプロセスを繰り返しながらコードの品質の向上を目指します(Refactor).
Fixtureはテスト用のデータのことです. 例えばHTTP通信でJSONを受け取るような場合に, 実際の通信相手がまだ存在しない, あるいは利用できない場合が考えられます. Fixtureとして定義しておくことでこうしたバックエンドの実装に左右されることなく, テストを実行できます.
Androidアプリ開発者を目指しています. 興味あることリスト: https://t.co/ew3bb6grdJ Github: https://t.co/9btqysHqWr Qiita: https://t.co/ZVRhjouauX
Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。
また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!
こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?
コメント