はじめに
2023年01月16日から大学のカリキュラムの一環として、iPresence合同会社にて企業内実習生として業務に関わらせて頂いている、大阪国際工科専門職大学情報工学科の2年生です。
学校では習ったことのない内容に四苦八苦しながら実装まで漕ぎつけた内容をqittaにまとめました。
この記事は、flutter/Dart のプロジェクトにおいて、初めて単体テストを作成した際に個人的に学習した内容をまとめたものとなります。説明用に簡潔かつ単純なケースを用いてテスト開発の基本的な概念を下記に示す。
ユニットテストの概要
まず初めに、単体テストとはプログラムを構成する比較的小さな単位(関数やメゾッド)で個々の機能が正しく動作するのか確認するテストである。個々の関数やメソッドの入力に対する出力が正しかどうかや、そもそもそれらが呼び出されているかどうかを確認してバグの原因をつぶしたり、どこまでが正しく機能が働いているのかを明確化する事が出来る。
ユニットテストの具体的な流れ
1. 事前準備
-
使用するパッケージ
・flutter_test
・mockito -
pubspec.yaml の dev_dependencies下に設定する
設定した後、flutter pub get
してパッケージの読み込みを行う。 -
テストファイルの準備
1.Flutterプロジェクタのtestフォルダ内にtestファイルを作成する。
2.テストファイルの名前はファイルの名前に_test
を付けて作成する。
これによりtestファイルと認識しているので必ずつける事。
3.テストファイルに必要なパッケージをimport
する。
2. 基本的なテストの書き方
ユニットテストの基本的な構成は以下のようになる。
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:flutter/material.dart';
import 'package:flutter_application_1/example.dart';
void main() {
group('example の test', () {
test('method1の test', () {});
test('method2の test', () {});
test('method3の test', () {});
});
}
-
group('テスト内容',(){})
test関数を1つのグループとしてまとめるメソッド -
test('テスト内容',(){})
ユニットテストの処理内容はこのtestメソッド内で記述する。
テストの単純な例としては以下のような形となる。
test('method1の test', () {
//... 事前処理
//関数の呼び出し や クラスのインスタンス化
except(テストする関数やメソッドが返す値・変数 , 期待する値);
});
test関数内で事前の処理をした後、関数やメソッドを呼び出した後にexcept()で処理後の値と、期待する値を比較することで、それらが正しい動きをしているのかを確認できる。
例えば、
・下記のメソッドは、select()
にTest クラス
を渡してstate
にインスタンス化したクラスを代入しています。
// 一部コードの処理を書き換えています。
class MyTestImpl extends MyTest {
MyTestImpl() : super(null);
@override
void select(Test test) {
state = test;
}
そのselect()
が上手く動作しているかを、以下のようにテストしている。
test('select の test', () {
// 1. 前処理
final container = ProviderContainer();
final myTest =container.read(MyTestProvider.notifier);
// 2. 関数呼び出し
myTest.select(Test(id: 'example0'));
// 3. except() で値の比較
expect(MyTest.state,Test(id: 'example0'));
});
1.前処理で、exampl.dart
のMyTestImpl
をインスタンス化し、myTest
に代入。
2.テストの部分で、myTest.select()
を呼び出し、Test(id: 'example0')
を引数に渡す。
3.expect()
でMyTestImpl.state
の値と、Test(id: 'example0')
を比較している。
4.myTest.select()
によって、state
にはTest(id: 'example0')
が代入されているので、テストは成功となる。
このようにクラス内メソッドや関数など、最小の構成ごとにテストしていく事で問題の切り分けが出来る。
3. Mockを用いたテスト
なぜMockを用いるのか?
上記のテストで、単純なテストは出来る。しかし、下記のような場合はどうだろうか
class MyTestImpl extends MyTest {
MyTestImpl() : super(null);
@override
void select(String id) {
//select内で別の関数が呼び出されている!
//id をもとに、Testクラスを返している。
state = mytestRepositry.select(id);
}
・今回の例では、メソッド内で別のクラスメソッドが呼び出されている。この状態でテストをすると、どのメソッドが正常に動いているのか明確にできない。ユニットテストの目的は個々のメソッドの振る舞いの成否を見ることなので、このままでは達成されない。そこで、用いられるのがMock化という仕組みである。
Mockとは
Mock化とは、正しい結果を返す、張りぼての関数を用意するという事である。
関数・メソッドが正しく動くとは、入力に対して正しい出力が返されるという事である。そのため、内部の処理に関係なく正しい入出力を行うものであれば元々の関数の代わりになると考えられる。そして、その関数の代わりとなるものを生成するのがMock化で生み出された物がMockである。Mockを用いることで、テストしたい関数に限定してテストを行う事が出来る。
また、代わりに用意する関数に正しい入出力を与える正常処理の他に、敢えて間違った出力をさせることで、その後の異常処理や例外処理のテストをも行う事も出来る。
まとめると、以下の2つの役割を持つ。
・正しい入出力を行い関数の代わりとなり、テスト範囲を限定する。
・代わりとなる関数に正しい入出力や間違った入出力を与えることでテストをコントロールする。
Mockの使用手順
テスト関数以外の関数やオブジェクトをMock化する手順は以下の通りである。
また、Mock化するのあたり今回はMockito
を使用した。
mytestRepositry.select()
をMock化しMyTestImpl.select()
をテストする処理を下記に示す
class MyTestImpl extends MyTest {
MyTestImpl() : super(null);
@override
void select(String id) {
state = mytestRepositry.select(id);
}
//1. Mock class の定義 --------------------------------------------
class MockMytestRepositry extends Mock implements MyTestRepositry{}
test('select の test', () {
//2. Mock class の呼び出し ----------------------------------------
final mockMytestRepositry = new MockMytestRepositry();
//3. スタブ ... Mockメソッドに、引数、帰り値を設定 -----------------
when(mockMytestRepositry.select('example0')).thenReturn(Test('example0'));
// 前処理
final container = ProviderContainer();
final myTest =container.read(MyTestProvider.notifier);
// テスト
myTest.select(Test(id: 'example0'));
//4. Mock化した関数が呼び出されたかどうか確認する。verifyメソッド ----
verify(mockMytestRepositry.select('example0'));
expect(MyTest.state,Test(id: 'example0'));
});
上記の処理を1つずつ確認していく。
1. Mock class の定義
class MockMytestRepositry extends Mock implements MyTestRepositry{}
MockクラスにMock化したいクラスをimplementsして、さらにそれをextendsすることで
Mock化したクラスを定義している。
2.Mock class の呼び出し
定義したMock化クラスを呼び出す。呼び出し方は、普通のクラスと同じ
3.スタブ Mockメソッドの引数、戻り値の設定
when(mockMytestRepositry.select('example0')).thenReturn(Test('example0'));
前述の~なぜMockが必要なのか~で示したように、
関数の代わりとして、入出力の正しく設定した張りぼてを用意する。
3.1. when()
で関数が呼び出す際の引数を設定している。
Mock関数である
mockMytestRepositry.select()
が呼び出された際の引数は'example0'
3.2. thenReturn()
で関数が呼び出された際の引数を設定している。
Mock関数である
mockMytestRepositry.select()
が呼び出された際の戻り値はTest('example0')
- 戻り値を設定するメソッドは返す値の型によって変える必要がある。
例えば、Stream
,Future
型を返す場合は、thenAnswer()
を使う必要がある。- 下記に、それぞれのデータ型を返す場合の基本的な書き方を示す。
// 戻り値がStream型の場合
when(mockMytestRepositry.select('example0')).thenAnswer((_)=>Stream.value(値));
// 戻り値がFuture型の場合
when(mockMytestRepositry.select('example0')).thenAnswer((_)=>Future.value(値));
// 戻り値がException型の場合
when(mockMytestRepositry.select('example0')).thenThrow(Exception);
さらに詳しく調べたい場合は ---> https://pub.dev/packages/mockito
4. Mock化した関数が呼び出されているか確認する。
mokitoのメソッドとして用意されている
verify(function)
では、引数にMock化した関数を渡すことでそれが呼び出されたかどうか確認できる。
verify(mockMytestRepositry.select('example0'));
注意点としては、3.のスタブで設定したMock化した関数と同じ物を渡す必要があるという事。つまり、スタブ時に渡した引数の値も一緒でないといけない。
おまけ build_runnerを用いたMockの自動生成。
build_runnerパッケージを用いたMockの自動生成
4.1.パッケージのインストール
dev_dependencies:
build_runner:^2.3.3
4.2.Mock化の準備
//1. Mock の自動生成 -------------------------------------------------
GenerateMocks([MytestRepositry])
test('select の test', () {
//2. Mock class の呼び出し ----------------------------------------
final mockMytestRepositry = MockMytestRepositry();
//3. スタブ ... Mockメソッドに、引数、帰り値を設定 -----------------
when(mockMytestRepositry.select('example0')).thenReturn(Test('example0'));
// 前処理
final container = ProviderContainer();
final myTest =container.read(MyTestProvider.notifier);
// テスト
myTest.select(Test(id: 'example0'));
//4. Mock化した関数が呼び出されたかどうか確認する。verifyメソッド ----
verify(mockMytestRepositry.select('example0'));
4.3.Mock自動生成
ターミナルから以下のコマンドを打つ
flutter pub run build_runner build
もしくはVScodeであれば、Ctr
+Shift
+B
で上記のコマンドが選択できるのでそこからでも可
4.4.Mockクラスをimport
Mockを自動生成するとMockファイルが生成される。そこのMockクラスをインポートする。
// Mockクラスをインポート
import 'example.mocks.dart';
GenerateMocks([MytestRepositry])
....
以上で、Mockの自動生成が完了する。これ以降の処理は前述と同じ。
まとめ
以上のように、Mockを使ったテストは
1. テストする関数以外をMock化
2. Mock化した関数に引数と戻り値を設定する。
3. テストする関数を呼び出す。
4. 関数の戻り値が妥当か、もしくはテスト関数内でMock化関数が呼び出されているか確認する
の手順で行われる。
以上より、今回の実習で学んだ事のまとめを終わりとする。
参考
- Mokito
https://pub.dev/packages/mockito - FlutterのNull safetyに対応したMockitoの基本的な使い方
https://zuma-lab.com/posts/flutter-mockito-null-safety-unit-test - Mockito入門 ~モックとスタブ~
https://crieit.net/posts/Mockito - Flutterでの単体テスト(完全版)
https://qiita.com/ko2ic/items/78c4a035a0aef9cfc78d - 【Flutter】Unit Testの基本的なやり方と業務で使えるUnit Test(Mockitoを使ったUnit Test)
https://blog.pentagon.tokyo/2387/