自動テストをより活用するためにミューテーションテストを使ってみよう - Flutter/Dart Test Talk Vol.1の登壇内容+一部加筆
Flutter/Dart Test Talk Vol.1で「DartでMutation testingをしてみよう」というタイトルで話してきました。
本稿では、発表資料をベースにしつつも足りない情報を加味した上でまとめていきたいと思います。
ミューテーションテストとは
そこまで新しい手法ではないため、名前自体は聞いたことがあるかもしれませんが、ミューテーションテストとは次をおこなえる手法です。
「テストコードの十分さを測定するための手法」
どうやって測定するかというと、プロダクトコードに変異を加えた上でテストコードを実行し、その箇所を見つけられるかをおこないます。
変異についてもう少し説明すると、次のようなコードがあったとします。
この中でミスをしやすいのが不等号になるかと思います。
// before
if (user.age > 60 ) {
point = point * 2;
}
// after
if (user.age == 60 ) {
point = point * 2;
}
このbefore、afterのように「>」を「==」に変更したときにテストコードで見つけられるかどうか(テストコードが失敗するかどうか)になります。
テストコードを実装するときは、上記のような境界値などは意識するかと思いますが、常に意識できているかというとそうでもないと思います。
こういった点を見つけてくれるものになります。
変異のパターンは色々と分けられます。
例えば次のようなものがあります。
AOR:算術演算子置き換え
a + b → a-b などに置き換え
LCR:論理結合子置き換え
a &&b → a || b などに置き換え
ROR:関係演算子置き換え
a > b → a < b などに置き換え
では実際に試したいとなったときにどうすればいいのでしょう?
ミューテーションテストを試せるパッケージ
上述したような変異を入れてテストコードを実行するのをサポートしてくれるパッケージはいろいろな言語にあります。
今回は「Flutter/Dart Test Talk」でしたのでDartのパッケージを紹介しました。
導入と実行は次のような感じです。
// 導入
$ dart pub add --dev mutation_test
// 実行
$ dart run mutation_test sample_config.xml
設定ファイル(sample_config.xml)を用意しておき、それを指定することで実行することができます。
出力結果はHTML、JUnit形式、MarkdownなどいろいろありますがHTMLだと次のような形で結果が出力されます(なお画像は全体の一部です)。
どれぐらい変異(38個)を入れて、それがどれぐらい見つかったか(36個)というのがわかります。
なお、iOSプロジェクトでも試せる次のようなライブラリもあります。
ただiOSの自動テストはシミュレーターを必要とするため、今回のような変異を入れつつテストコードを実行するという行為は実行時間がかかりやすくなるのが難点といえます。
サンプルコードを片手に見てみる
購入金額に応じてポイントを返すコードを実装したとします。
その際の仕様は次とします。
100円で1ポイント
60歳以上だとポイントが2倍
ポイントの最大値は200
その時のサンプルコードが次とします。
int point(int price, User user) {
if (money <= 0) {
throw ArgumentError('0円以下はありえません。');
}
// 100円で1ポイント
int point = price~/100;
// 60歳以上は2倍
if (user.age >= 60) {
point = point * 2;
}
// ポイントの最大値は200
if (point > 200) {
point = 200;
}
return point;
}
このときのテストってどうしますか?
60歳という境界と200ポイントという境界を「多少」意識した上で次を列挙したとします。
59歳で100円購入したら1ポイント
60歳で100円購入したら2ポイント
60歳で10000円購入したら200ポイント
サンプルのテストコードは次のような感じです。
これでテストコードは足りていますか?
これに対してミューテーションテストをおこなった結果は次のとおりです。
2箇所の変異を見つけることができていません。
1つ目は60歳以上の場合はポイント2倍のケースです。
仮に「>=」が「==」となった場合はテストコードでは見つけることができません。
今のテストコードでは59歳と60歳のみ見ており、61歳は見ていません。
したがって上の変異については見つけることはできません。
2つ目はポイントの最大値が200でなくて違った場合です(今回の変異は -200というマイナスの値)。
今のテストコードでは200ピッタシのテストケースしか用意していないため、pointが201になるようなものは見ておらずここを通っていません。
(この点についてはコードカバレッジの結果でも気づくかと思います)
指摘された上記の2点を考慮したテストコードは次になります。
なお、蛇足ですがこのように列挙し続けるとテストコードの認知コストが高くなりがちになるので、パラメタライズドテストを活用して見やすくするのが良いです。
このテストコードを元に再度ミューテーションテストを実行すると、次のように100%となります。
これらを見て分かるように、テストコードについてのFBをもらうことができます。
ミューテーションテストの課題
一見、便利なように見えるミューテーションテストですが利用する上での課題はいくつもあります。
例えば次のようなものがあります。
実行時間
直す価値のある指摘かのチェック
実行時間
「プロダクトコードに変異を加えてテストコードを実行する」
これを何パターンもおこなうわけですから実行時間がかかるのは想像できるかと思います。
PR単位やmerge単位で実行を繰り返していると実行時間がかかってしまいます。
そして、今はCI/CDサービスは従量課金時代です。
したがって何も気にせず実行し続けていると気づいたら課金額がエライことになるかもしれません。
そのためPR時には「変更があったファイルのみ」を対象とするなど実行する範囲を絞る必要があります。
またそうやって絞れるようなテストコードの作りである必要もあります。
とはいえ、以前と比べてマシンスペックはよくなっているためこういった手法も活用できる見込みがたってきているのではないかと思います。
直す価値のある指摘かのチェック
ミューテーションテストにおける変異は今まで書いてきたとおりです。
等価ミューテーション
もしかしたら気づいた方もいるかもしれませんが、振る舞いが変わらないケースというのもあります。
// before
min(a, b)
// after
min(b, a)
たとえば、よくある小さい方を返すminメソッドがあったとして、その引数を交換した場合は「振る舞い」がbefore/afterで変わりません。
これをテストコードで見つけることはできません。
こういうのを「等価ミューテーション」と呼びます。
テストコードが不必要な箇所の指摘
運用のためなどに次のように「特定条件下」でログを出力するということもあるかと思います。
// 特定条件下でログを出力する
if user.age <= 10 {
log.info("ageが10以下となっています")
}
このログ出力までテストコードで確認しているでしょうか?
テストコードは必要なところにあればよくて、なんでもかんでもテストコードを実装すれば良いわけではありません。
しかし、(一般的に)そういったものを加味した上で変異を組み込んでいくわけではありません。
上述したように必ずしも指摘された箇所が直すべき箇所とは限りません。
変異を見つけた%を出力してくれますが、その数値を元に何かしら閾値をもうけることに「意味があるのか?」というのはあります。
特定のファイルは%が高くできる傾向のところもあるでしょうが、逆の箇所もあるはずです。
したがって結果の詳細を見る必要性があることが多いです。
ここらへんを考慮した上でより有効活用していくためには、自分たちのプロダクトの作りに応じてミューテーションテスト自体を作り込んでいく必要があるかもしれません。
おわりに
ミューテーションテスト自体はそこまで新しい話ではありませんが、昨今のマシンパワーを考えると実践でも使える機会が出てきているかもしれないです。
特に自動テストが(特定領域においては)当たり前になりつつある昨今において、コードカバレッジといった指標と併せて活用するというのもあるかと思います。
しかし、まだまだ実務での導入事例や運用事例はあまり聞かないのが実情です。
是非とも試していただいて、その試行錯誤の結果をアウトプットしてもらえると私としても嬉しい限りです。
参考資料
この記事が気に入ったらサポートをしてみませんか?