究極にして王道のテスト技法である同値分割を自動テストに利用する


はじめに

テスト駆動開発が広まってきました。
同時にテストコードを書く文化も広まってきました。
テストコードを書いているけれども、テストコードがバグを見つけることができているのか、
不安ではないでしょうか。
テスト駆動開発はテストの技法ではないといわれますが、
せっかくテストコードを書いているので、バグも見つけたいですよね。
そんなときに役に立つのが、同値分割と呼ばれるテストの技法です。

この記事では、同値分割がどのように自動テストに役に立つか解説します。

同値分割とは

同値分割をとても簡単に説明すると、
「入力を同じ処理をするグループに分けることで、テストの数を減らす方法」です。
普段、テストをする際に、同値分割と呼ぶことは知らなくとも、皆さん使用されているかと思います。

詳しく同値分割について知りたい方は、下記の資料を、参照してください。
解説:https://gihyo.jp/dev/serial/01/tech_station/0004
演習:https://amzn.to/35QE68E

同値分割こそ、究極にして王道なテスト技法

同値分割は、どのテストレベル(単体テスト、結合テスト、システムテストなど)でも利用できる技法です。
自動テストにもとても相性の良い技法です。

この同値分割を制するものが、ソフトウェアテストを制するといっても問題ないでしょう。
同値分割は奥が深く、同値分割を上手に行えないと、バグを見逃してしまいます。
同値分割を上手に行うのは難しく、同値分割を上手にできるているかがテストへの良し悪しの重要なポイントになります。
手動で行うテスト実行も自動て行うテスト実行もどちらの場合でも同じです。

コードカバレッジ100%

テストコードで、コードカバレッジ100%になってしまっても全ての同値なグループを見つけていないとバグを見逃してしまいます。
コードカバレッジ100%で確認できることは、テストコードがプロダクトコードをすべて網羅しているということだけです。

プロダクトコードをすべて網羅するテストコードは、確かに多くのバグを見つけることができますが、
特定の入力の場合にのみ、起こるバグがあります。
その入力を特定できていない場合は、コードカバレッジ100%でも見つかりません。
コードカバレッジ100%を目指すのではなく、良いテストコードを書くことを目指す必要があります。

グレゴリオ暦のうるう年を判定するプログラムの例

グレゴリオ暦のうるう年判定を例に説明していきます。

うるう年を判定するアルゴリズムは、下記のとおりです。

  • 西暦年が4で割り切れる年はうるう年
  • ただし、西暦年が100で割り切れる年は平年
  • ただし、西暦年が400で割り切れる年は必ずうるう年

コードにする

JavaScriptで書きます。
(私が、JavaScriptばっかりつかっているので、JavaScriptで書きます)

正解のコードは、下記になります。

isLeepYear.js
function isLeepYear(year){
    if(year % 4 === 0 && year % 100 !== 0 || year % 400 === 0){
        return true;
    } else {
        return false;
    }
}
isLeepYear.js
function isLeepYear(year){
    if(year % 4 === 0 && year % 400 === 0){
        return true;
    } else {
        return false;
    }
}

ステートメントカバレッジを100%にするには、下記のテストコードで良いのです。
ブランチカバレッジも同じテストコードで100%になります。

FizzBuzz.spec.js


test('うるう年のとき', () => {
  expect(isLeepYear(2004)).toBe(true);
}

test('平年のとき', () => {
  expect(isLeepYear(2001)).toBe(false);
}

これでは、次のような誤ったコードでも通ってしまいます。

isLeepYear.js
function isLeepYear(year){
   // || 以下を書いていなかった
    if(year % 4 === 0 && year % 100 !== 0 ){
        return true;
    } else {
        return false;
    }
}

このように、テストコードを十分に書くことができないと、
リファクタリングした場合に新しいバグを埋め込だり、
テストファーストを行っている場合に実装を漏らしてしまいます。

この問題は、同値分割を正しく行うことで、防ぐことができます。

うるう年判定プログラムを同値分割する

同値分割すると、次のようになります。

うるう年

  • 西暦年を4で割り切れる年で、西暦年を100で割り切れる年で、西暦年を400で割り切れる年 :例 2000年
  • 西暦年を4で割り切れる年で、西暦年を100で割り切れない年で、西暦年を400で割り切れない年 :例 2004年

平年

  • 西暦年を4で割り切れる年で、西暦年を100で割り切れる年、西暦年を400で割り切れない :例 2100年
  • 西暦年を4で割り切れない年で、西暦年を100で割り切れない年で、西暦年を400で割り切れない年:例 2001年

これをテストコードに書くと次のようになります。

FizzBuzz.spec.js

describe('うるう年のとき',()=>{
  test('西暦年を4で割り切れる年で、西暦年を100で割り切れる年で、西暦年を400で割り切れる年', () => {
    expect(isLeepYear(2000)).toBe(true);
  })
  test('西暦年を4で割り切れる年で、西暦年を100で割り切れない年で、西暦年を400で割り切れない年', () => {
    expect(isLeepYear(2004)).toBe(true);
  })
})
describe('平年のとき',()=>{
  test('西暦年を4で割り切れる年で、西暦年を100で割り切れる年、西暦年を400で割り切れない', () => {
    expect(isLeepYear(2001)).toBe(false);
  })
  test('西暦年を4で割り切れない年で、西暦年を100で割り切れない年で、西暦年を400で割り切れない年', () => {
    expect(isLeepYear(2100)).toBe(false);
  })
})

同値分割の進め方

ここまで読んだ方は、「いや同値分割どうやって整理したんだよ!」と思われたかと思います。
うるう年判定ですら同値分割を行うことは難しいです(記事を書いていて、大変でした)。

うるう年判定プログラムの場合、デシジョンテーブルを作ることで、同値分割を素早く間違いなくできます。
これは、複雑な条件と動作が決まっているからです。

デシジョンテーブルは次のようになります(省略した形)。

1 2 3 4
西暦を4で割り切れる Y Y Y N
西暦を100で割り切れる Y Y N N
西暦を400で割り切れる Y N N N
うるう年 X - X -
平年 - X - X

デシジョンテーブルを使うことで、複雑な条件をモデル化することができます。
これをそのままテストケースに起こすことで、同値分割できたテストコードを書くことができます。

実世界のプログラミングで、同値分割を上手に行うことは、もっと難しいです。
結合テストなど、テストレベルが上がっていくと、さらに複雑になり難しくなります。

同値分割は、メソッドというよりは考え方の様なものだと思っています。
同値分割を上手くやるために利用できるメソッドが、他にも複数あります。
(メソッドとは呼べないかもしれません)

  • デシジョンテーブル
  • 組み合わせテスト(Pair-Wiseなど)
  • 状態遷移テスト
  • 原因結果グラフ
  • マインドマップを利用する
  • ラルフチャート
  • クラフィケーションツリー
  • 意地悪漢字

詳しく知りたい方は、ご自身で検索してください。

さいごに

最後になりましたが、
今回の記事の言葉は、JSTQBの用語に合わせず、なるべく一般的な言葉を選択しました。

同値分割を行うことで、自動テストの質をよくできます。
同値分割をサポートするテストの方法が、たくさん提案されています。

どんなテストコードを書けば良いのか、道標になれれば幸いです。

参考になりそうなURL

次に読むと参考になりそうなURLを貼っておきます。
http://qualab.jp/materials/q_te.140528.color.pdf
https://thinkit.co.jp/article/15287
https://thinkit.co.jp/article/15366
https://testing.googleblog.com/2008/03/tott-understanding-your-coverage-data.html?m=1