はじめに
今読み進めている「関数型プログラミングの基礎 JavaScriptを使って学ぶ」の中で紹介されていた Property based testing に興味がわいたのでJavaScript環境で試した話です。
なお、Property based testing とはなんぞや という話については、本稿では詳しく触れません。そちらを知りたい方は、諸先輩方が投稿しているQiita記事などで調べて見てください。
環境
-
Node.js : v14.17.0
-
npm : v6.14.13
-
jest : v27.0.1
- https://jestjs.io/ja/
- Facebook のオープンソース
- フルスタックテスティングフレームワーク
-
fast-check : v2.14.0
- https://dubzzz.github.io/fast-check.github.com/
- Property based testing のフレームワーク
-
jest-fast-check : v1.0.2
- https://github.com/dubzzz/jest-fast-check
- jest と fast-check の統合用ライブラリ(記述を簡素化できるだけで必須ではない)
準備
作業ディレクトリを用意
(Node.js はインストール済みの前提です。)
$ mkdir property-based-testing
$ cd property-based-testing
$ npm init --yes
ライブラリをインストール
$ npm install --save-dev jest fast-check jest-fast-check
テスティングフレームワークの使用準備
編集前
{
"name": "property-based-testing",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"fast-check": "^2.15.0",
"jest": "^27.0.3",
"jest-fast-check": "^1.0.2"
}
}
"scripts"
の "test"
を以下のように書き換える
"scripts": {
"test": "jest"
},
実践
題材
「関数型プログラミングの基礎 JavaScriptを使って学ぶ」 でも使われている 後者関数 を使って以下の3つのケースに分けて各テスティングフレームワークを使って行きます。
- jest のみを使用
- jest + fast-check を使用
- jest + fast-check + jest-fast-check を使用
テスト対象
const succ = (x) => { return x + 1; };
テスト内容
上記関数において succ(0) + succ(x) = succ(succ(x))
という命題をProperty based testingを使って確認する
共通部分
3つのケースで共通となるテスト対象の関数を作成します。
$ mkdir src
$ touch src/app.js
const succ = (x) => {
return x + 1;
};
module.exports = {
succ,
};
jest のみ使用
テストコードを追加します
$ mkdir test
$ touch test/app-jest.test.js
const { succ } = require('../src/app');
const iterate = (init) => {
return (step) => {
return [init, (_) => {
return iterate(step(init))(step);
}];
};
};
/* 無限の整数列を生成する */
const enumFrom = (n) => {
return iterate(n)(succ);
};
/* ストリームのmap関数 */
const map = (transform) => {
return (aStream) => {
console.log(aStream);
const head = aStream[0];
return [transform(head), (_) => {
return map(transform)(aStream[1]());
}];
};
};
/* ストリームの先頭から引数n分だけ取り出す */
const take = (n) => {
return (aStream) => {
if (n === 0) {
return null;
} else {
return [aStream[0], (_) => {
return take(n-1)(aStream[1]());
}];
}
};
};
/* ストリームの全ての要素がtrueであるか判定する */
const all = (aStream) => {
const allHelper = (aStream, accumulator) => {
const head = aStream[0];
const newAccumulator = accumulator && head;
if (aStream[1]() === null) {
return newAccumulator;
} else {
return allHelper(aStream[1](), newAccumulator);
}
};
return allHelper(aStream, true);
};
/* 検証の対象となる命題 */
const proposition = (n) => {
return succ(0) + succ(n) === succ(succ(n));
};
/* 100個の整数について命題が正しいか */
test(
'should succ(0) + succ(x) = succ(succ(x))',
() => {
expect(
all(
take(100)(
map(proposition)(enumFrom(0))
)
)
).toBe(true);
}
);
テストの実行は以下のコマンドで行います
$ npm test
上記のコードでは、1から100の100個の整数で命題が正しいかチェックしています
このようにProperty based testingはフレームワークを使用しないでも実現することができます
ただし、整数をランダムに生成したり、事前条件などを追加したりしようとすると柔軟性にかけます
つまり、検証用のテストデータ生成部分の柔軟性に難がある(手間がかかる)ということです
jest + fast-check を使用
では、Property based testingのフレームワークを使用した場合にどうなるかみてみます
なお、fast-check の詳細は公式を確認してみてください
$ touch test/app-fast-check.test.js
const { succ } = require('../src/app');
const fc = require('fast-check');
test(
'should succ(0) + succ(x) = succ(succ(x))',
() => fc.assert(
fc.property(
fc.integer({ min:1 }),
(x) => {
// console.log(x);
expect(succ(0) + succ(x)).toBe(succ(succ(x)));
}
)
)
);
fast-checkの各関数の詳細については、公式およびGithubを参照してください
上記コードではfc.integer()
が検証用のテストデータを生成している部分になります
コメントアウトしてる部分を有効にすると、1以上のランダムな整数がテストデータとして使われていることが確認できます
jest + fast-check + jest-fast-check を使用
fast-check を使うことにより、テストデータの生成がとてもシンプルに記述することができました
ただ、fast-check のみ使用した場合だと、fc.assert()
やfc.property()
など、fast-check独自の記法が必要となり、jestとの記述の差異が生まれます
この差異を埋めるのがjest-fast-checkです
jest-fast-checkの詳細はGithubを参照してください
jest-fast-checkを使ったコードが以下になります
$ touch test/app-jest-fast-check.test.js
const { succ } = require('../src/app');
const { testProp, fc } = require('jest-fast-check');
testProp(
'should succ(0) + succ(x) = succ(succ(x))',
fc.integer({ min:1 }),
(x) => {
expect(succ(0) + succ(x)).toBe(succ(succ(x)));
}
);
jest-fast-checkのソースコードを読むとわかりますが、
testProp()
がfc.assert()
とfc.property()
をtest()
内で実行するようなラッパーになっています
testProp()
を使うと記述量を抑え、よりjestの記法に近い形でProperty based testingのコードを記述できます
参考記事
今回Property based testingを学習する際にこちらの記事を参考にさせていただきました