本記事は、サムザップ #2 Advent Calendar 2019 の12/9の記事です。
はじめに
テスタビリティ(Testability, テスト容易性)とはテストがどれだけ実行しやすいか、どれだけ効果的かを表す度合いです。
テスタビリティが高いとより不具合を見つけやすく、コードの品質を高めることができます。
テスタビリティの特徴としてはJames Bachがあげたものがあります。
テスタビリティが高いコードを書くことは、それ自体がコードの品質を高めることになる場合があります。
参照透過性を例に説明していきたいと思います。
参照透過性をもつ関数はテスタビリティが高いと言われています。
参照透過性を壊すものの一部を挙げると以下があります。
- 環境変数
- グローバル変数
- 現在時刻を返す関数
- 乱数生成機
これらを含む関数は実行する環境やタイミングなどによって返す値が異なり、テスタビリティの低い関数になることがあります。
例
例として、様々な敵をランダムに生成し返す関数createRandomEnemy()
を考えます。
この関数は呼ばれる度に95%の確率でノーマル敵、5%の確率でレア敵を返すという仕様だとして、
以下のように書いたとします。
const createRandomEnemy = () {
const probability = Math.random();
if (probability > 0.05) {
return new nomalEnemy(); // ノーマル敵を表すオブジェクト
}
return new rareEnemy();// レア敵を表すオブジェクト
}
仕様通りの動作をするか確認するには、以下のように対象の関数を何万回も呼び出した上で5%程度の範囲内かどうかを調べるテストが考えられます。
しかし、これでは確率的に成功/失敗するテストになってしまいます。
test('レアな敵の出現チェック.', () => {
enemies = createEnemies(50000); // たくさん敵を生成する. 内部でランダムに敵を生成する関数を呼び出している
const resut = aggregateEnemies(enemies); // どの敵がどれだけ生成されたかを集計する関数(略)
expect(resut.rareEnemyWeight).toBeWithinRange(0.0499, 0.0501); // レア敵が5±0.1%程度で生成されているかのアサーション
});
function createEnemies(count) {
const enemies = [];
_.times(count) {
enemies.push(createRandomEnemy());
}
return enemies;
}
関数内で行っていた乱数生成Math.random()
をやめ、
createRandomEnemy()
に外から乱数を与えるようにし参照透過性をもたせることで、これを防ぐことができます。
つまり、createRandomEnemy()
の関数呼び出しの結果が、引数probability
にのみ依存するようにします。
const createRandomEnemy = (probability) {
if (0.05 < probability) {
return new nomalEnemy();
}
return new rareEnemy();
}
こうすることで、createRandomEnemyについては何度実行しても同じ結果となります。
test('レアな敵の出現チェック.', () => {
const enemy = createRandomEnemy(0.01);
expect(enemy.type).toEqual(RARE);
});
まとめ
参照透過性を保つことはコードを理解しやすくし、テスタビリティを高め、不具合を減らす上で役立ちます。
簡単な工夫で対処できる場合は、テスタビリティの観点をもってコードを書くことをおすすめします。
明日は @tomokazu_kurosawa さんの記事です。