Strykerを使ってTypeScriptでMutation Testingする
概要
JavaScript/TypeScript向けのMutation Testing FrameworkであるStryker-jsがいい感じになってきたので紹介してみます。簡単な紹介のみです。
対象読者
- Strykerの概要を知りたい人
 - Mutation Testingよく知らないJSer/TSer
 - Mutation Testing導入したいけどJS/TS用のツールが見つからなかった人
 
Mutation Testingとは
テスト対象のコードを変更(Mutate)するミュータントを仕込み、Unit Testが正しくそれらのミュータントを退治できるか測定するものです。
一般的には退治できた割合が高いほど意味のあるUnit Testを書いていると言えます。
Wikipediaの項目にもある通りFuzzingの一種とも言えます。
Mutation Testing自体はGoogleが論文出してたり最近Testing Blogでも取り上げられたりとそれなりに一般的な手法です。
JS/TSで以前やろうとしたときはあまり良いものが見つからなかったのですが、当時から目をつけていたStrykerが5系でいい感じになってきたので紹介してみます。
Strykerとは
JS/TS, C#, Scalaで使えるMutation Testing Frameworkです。
今回紹介するStryker-jsはTypeScriptで書かれていてJSとTSのコードベースで利用することができます。
今回はJestを使いますがKarmaやJasmine等のRunnerもあります。
また今回は紹介しませんがReactプロジェクトやVue、Angular等もサポートされています。
使ってみる
今回使うコードはこちらにあります。
Jestでテストを書く
まずはJest公式のGetting Startedに従って足し算をするだけの関数とそのUnit Testを書きます。
yarn add --dev jest
function sum(a, b) {
  return a + b;
}
module.exports = sum;
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});
公式の通りに書いたのでここでテストを走らせると問題なく通ります。
$ jest
yarn run v1.22.5
warning package.json: No license field
$ jest
 PASS  ./sum.test.js
  ✓ adds 1 + 2 to equal 3 (1 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.631 s
Ran all test suites.
Done in 1.01s
Strykerを導入する
続いてStrykerのgetting started guideに従ってStryker-jsを導入します。
yarn global add stryker-cli
yarn add -D @stryker-mutator/core
stryker init
最後のstryker initのあといくつかの質問に答えるとstryker.conf.jsという設定ファイルが出来上がります。
設定項目の詳細は https://stryker-mutator.io/docs/stryker-js/configuration を参照してください。
stryker runを実行するとターミナル上に結果が表示されるとともに以下のようなhtml形式のレポートがreports/mutation/html/index.htmlに生成されます。
今回は関数もテストもシンプルだったのでMutation Testも通ってしまいました。

それぞれの番号または"Expand All"ボタンをクリックするとどのようなミュータントが退治されたのか確認できます。

0番目のミュータントは関数の中身を空({})にしてしまっています。
1番目のミュータントはreturn a + bという戻り値をreturn a - bに書き換えてしまっています。
今回の場合はこれらのミュータントが仕込まれた場合Unit Testが失敗している(=ミュータントを退治できた)ため、いずれもkilledになっています。
今回の2つ以外にどのようなMutationが行われるかは公式サイトにリストがあります。
また設定ファイルで除外するMutationを指定することも可能です。
テストを失敗するように書き換えてみる
作為的ではありますが、Mutation Testが失敗するようにUnit Testを以下のように書き換えます。
test('adds 0 + 0 to equal 0', () => {
  expect(sum(0, 0)).toBe(0);
});
Unit Testとしてはこれでも通るのでjestを実行すると以下の通り成功します。
$ jest
 PASS  src/sum.test.js
  ✓ adds 0 + 0 to equal 0 (2 ms)
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.395 s, estimated 1 s
Ran all test suites.
しかしstryker runをすると以下の通り失敗します。

今回は作為的にテストケースを0 + 0 = 0にしてしまったため、+を-に変えるミュータントは退治できていないことが分かります。(0 + 0 = 0も0 - 0 = 0も成立するため)
このようにカバレッジは高いけど意味のないUnit Testを書いてしまっているような状況を改善するのにMutation Testingは役立ちます。
ただし一般的にMutationは実際の環境で起こる不具合よりも単純なものであるため、Unit Testのカバレッジ100%を目指すのが常に正しいとは限らないのと同様、全てのミュータントを退治することが常に正しいとは限りません。
- 参考
 
TypeScriptチェッカーを導入する
まずは書き換えたUnit Testを最初のものに戻してください。
そして必要なDependenciesを追加します。
今回はts-jestではなくbabelを使います。ググった様子だとts-jestでも動くはずです。
yarn add --dev typescript babel-jest @babel/core @babel/preset-env @types/jest @babel/preset-typescript
babelの設定ファイルを以下のように作成します。
module.exports = {
  presets: [
    ['@babel/preset-env', {targets: {node: 'current'}}],
    '@babel/preset-typescript'
  ],
};
次に関数を以下のようにTypeScript版に書き換えます。
function sum(a: number, b: number): number {
  return a + b;
}
export default sum;
公式サイトの通りにStrykerのTypeScriptチェッカーを追加します。
yarn add -D @stryker-mutator/typescript-checker
stryker.conf.jsに以下の2行を追加します。
{
  "checkers": ["typescript"],
  "tsconfigFile": "tsconfig.json"
}
stryker runを実行すると以下のような結果になります。

JavaScriptで実行したときに作られた関数の中身を空({})に変えてしまう0番目のミュータントはTypeScriptの関数の戻り値の型として成立しないのでコンパイルエラーとなるため生成されていないことが分かります。
このようにTypeScriptのコードベースの場合はTypeScriptチェッカーを使うと不要なミュータントの生成を抑えることができテスト時間を削減することができます。
最後に
ということでStriker-jsの簡単な紹介でした。
JS/TSでMutation Testingの導入を考えてる方の参考になれば幸いです。
導入が簡単なのでとりあえず試してhtmlレポートを見てみるのもオススメですがテスト数が多いとだいぶ時間かかるので設定ファイルで対象を絞る等した方が良いかもしれません。
Happy Testing!
Discussion