堅牢なソフトウェアを作ろうと思ったら、テストは欠かせないプロセスだ。
ソフトウェアテストというのは、要するに「ちゃんと動くかどうか」を確認する作業なのだが、「使ってみました。たぶん問題ありません。」という簡単な話でもない。
私もあまりテストに関して知識が無かったので、以下の書籍を読んだ。
- 作者: 高橋寿一
- 出版社/メーカー: 翔泳社
- 発売日: 2013/12/10
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (7件) を見る
語り口が軽快でサクサク読めるので、テストに興味があるけれどまったく初めてという人にお勧め。
さて、この本で学んだ知識(同値分割法・境界値分析法)を基にFizzBuzzのテストを書いてみようと思う。
まずは、普通のFizzBuzzコード
Sub FizzBuzz() For i = 1 To 100 Select Case 0 Case i Mod 3 + i Mod 5 Debug.Print "FizzBuzz" Case i Mod 3 Debug.Print "Fizz" Case i Mod 5 Debug.Print "Buzz" Case Else Debug.Print i End Select Next End Sub
問題なく動作するが、戻り値が無いのでテストができない。
これをテスタブルにするためには、Functionプロシージャで関数化すれば良い。
Function fnFizzBuzz(x) As String Dim ret As String Select Case 0 Case x Mod 3 + x Mod 5 ret = "FizzBuzz" Case x Mod 3 ret = "Fizz" Case x Mod 5 ret = "Buzz" Case Else ret = CStr(x) End Select fnFizzBuzz = ret End Function Sub TestableFizzBuzz() For i = 1 To 100 Debug.Print fnFizzBuzz(i) Next End Sub
すると、fnFizzBuzz関数のテストを書くことができる。
まずは簡単なテストから。
Sub TestfnFizzBuzz1() Debug.Assert fnFizzBuzz(6) = "Fizz" Debug.Assert fnFizzBuzz(10) = "Buzz" Debug.Assert fnFizzBuzz(30) = "FizzBuzz" Debug.Assert fnFizzBuzz(16) = "16" MsgBox "テスト完了" End Sub
Debug.Assertは、Falseになるとコードが中断する命令である。
つまり、コードが中断せずに最後のメッセージ「テスト完了」が出たら、テストにパスしたということだ。
テストする値は、同値分割の考え方でFizzになる数値グループ、Buzzになる数値グループ、FizzBuzzになる数値グループ、数値のままに表示される数値グルーブに分けて、グループ代表の数値ひとつをピックアップする考え方である。
逆に、同値分割の考え方だけなら、同じグループでいくつも書く必要はない。
ただし、0はよくバグの元になるので、必ずテストする。
また、3、5、15はそれぞれ最小値のグループの最小値になるので、境界値と考えて良いのだろうか。
あまり自信がないが境界値分析法のつもりで一応入れておく。
また、Fizz以下の最大値2も加えてみた。
Sub TestfnFizzBuzz2() Debug.Assert fnFizzBuzz(0) = "FizzBuzz" Debug.Assert fnFizzBuzz(2) = "2" Debug.Assert fnFizzBuzz(3) = "Fizz" Debug.Assert fnFizzBuzz(6) = "Fizz" Debug.Assert fnFizzBuzz(5) = "Buzz" Debug.Assert fnFizzBuzz(10) = "Buzz" Debug.Assert fnFizzBuzz(15) = "FizzBuzz" Debug.Assert fnFizzBuzz(16) = "16" Debug.Assert fnFizzBuzz(30) = "FizzBuzz" MsgBox "テスト完了" End Sub
これもパス。
次に、負数を与えてみたらどうなるのか。
数学的には、負数の余り算も成り立つので、パスしてくれないと困る。
マイナスゼロなんてのは無いけれど、一応いれてみた。
Sub TestfnFizzBuzz3() '正の数 Debug.Assert fnFizzBuzz(0) = "FizzBuzz" Debug.Assert fnFizzBuzz(2) = "2" Debug.Assert fnFizzBuzz(3) = "Fizz" Debug.Assert fnFizzBuzz(6) = "Fizz" Debug.Assert fnFizzBuzz(5) = "Buzz" Debug.Assert fnFizzBuzz(10) = "Buzz" Debug.Assert fnFizzBuzz(15) = "FizzBuzz" Debug.Assert fnFizzBuzz(16) = "16" Debug.Assert fnFizzBuzz(30) = "FizzBuzz" '負の数 Debug.Assert fnFizzBuzz(-0) = "FizzBuzz" Debug.Assert fnFizzBuzz(-2) = "-2" Debug.Assert fnFizzBuzz(-3) = "Fizz" Debug.Assert fnFizzBuzz(-6) = "Fizz" Debug.Assert fnFizzBuzz(-5) = "Buzz" Debug.Assert fnFizzBuzz(-10) = "Buzz" Debug.Assert fnFizzBuzz(-15) = "FizzBuzz" Debug.Assert fnFizzBuzz(-16) = "-16" Debug.Assert fnFizzBuzz(-30) = "FizzBuzz" MsgBox "テスト完了" End Sub
これも無事にテストパス。
次にLong型の最大値境界と最小値境界をテスト
Sub TestfnFizzBuzz4() '~既出テストは省略~ 'Long型の最大値境界 Debug.Assert fnFizzBuzz(2147483647) = "2147483647" Debug.Assert fnFizzBuzz(2147483648#) = "2147483648" 'Long型の最小値境界 Debug.Assert fnFizzBuzz(-2147483648#) = "-2147483648" Debug.Assert fnFizzBuzz(-2147483649#) = "-2147483649" MsgBox "テスト完了" End Sub
おっと、ここでオーバーフローエラー
つまりfnFizzBuzzは、Long型の最大値2147483647を超えるとバグが発生する関数ということ。
ということで、関数本体を以下のように修正。
境界値を超えると"ERROR"という文字列を返すようにした。
Function fnFizzBuzz(x) As String Dim ret As String If x <= 2147483647 And x >= -2147483648# Then Select Case 0 Case x Mod 3 + x Mod 5 ret = "FizzBuzz" Case x Mod 3 ret = "Fizz" Case x Mod 5 ret = "Buzz" Case Else ret = CStr(x) End Select Else ret = "ERROR" End If fnFizzBuzz = ret End Function
テストも以下のように修正する。
Sub TestfnFizzBuzz5() '~既出テストは省略~ 'Long型の最大値境界 Debug.Assert fnFizzBuzz(2147483647) = "2147483647" Debug.Assert fnFizzBuzz(2147483648#) = "ERROR" 'Long型の最小値境界 Debug.Assert fnFizzBuzz(-2147483648#) = "-2147483648" Debug.Assert fnFizzBuzz(-2147483649#) = "ERROR" MsgBox "テスト完了" End Sub
これでテストは無事にパス。
次に、悪いデータのテストを行う。
悪いデータとは、本来想定されていないデータのことで、たとえば文字列、日付、小数などを受け取ったときにコードがどう振る舞うかをテストする。
出来上がった最終のテストコードはこちら。
Sub TestfnFizzBuzzFinal() '正の数 Debug.Assert fnFizzBuzz(0) = "FizzBuzz" Debug.Assert fnFizzBuzz(2) = "2" Debug.Assert fnFizzBuzz(3) = "Fizz" Debug.Assert fnFizzBuzz(6) = "Fizz" Debug.Assert fnFizzBuzz(5) = "Buzz" Debug.Assert fnFizzBuzz(10) = "Buzz" Debug.Assert fnFizzBuzz(15) = "FizzBuzz" Debug.Assert fnFizzBuzz(16) = "16" Debug.Assert fnFizzBuzz(30) = "FizzBuzz" '負の数 Debug.Assert fnFizzBuzz(-0) = "FizzBuzz" Debug.Assert fnFizzBuzz(-2) = "-2" Debug.Assert fnFizzBuzz(-3) = "Fizz" Debug.Assert fnFizzBuzz(-6) = "Fizz" Debug.Assert fnFizzBuzz(-5) = "Buzz" Debug.Assert fnFizzBuzz(-10) = "Buzz" Debug.Assert fnFizzBuzz(-15) = "FizzBuzz" Debug.Assert fnFizzBuzz(-16) = "-16" Debug.Assert fnFizzBuzz(-30) = "FizzBuzz" 'Long型の最大値境界 Debug.Assert fnFizzBuzz(2147483647) = "2147483647" Debug.Assert fnFizzBuzz(2147483648#) = "ERROR" 'Long型の最小値境界 Debug.Assert fnFizzBuzz(-2147483648#) = "-2147483648" Debug.Assert fnFizzBuzz(-2147483649#) = "ERROR" '文字列 Debug.Assert fnFizzBuzz("15") = "FizzBuzz" Debug.Assert fnFizzBuzz("150,000") = "FizzBuzz" Debug.Assert fnFizzBuzz("aa") = "ERROR" '日付 Debug.Assert fnFizzBuzz(Now) = "ERROR" Debug.Assert fnFizzBuzz(Date) = "ERROR" Debug.Assert fnFizzBuzz(Time) = "ERROR" '小数 Debug.Assert fnFizzBuzz(0.1) = "ERROR" Debug.Assert fnFizzBuzz(-0.1) = "ERROR" Debug.Assert fnFizzBuzz(1E-100) = "ERROR" MsgBox "テスト完了" End Sub
一旦本体はそのままでテストしてみると、Nowを渡したときにコードが中断した。
フム。。
"ERROR"を返すコードは書いてないが、Nowを渡すと例外が発生すると思っていた。
しかし、Now Mod 3とすると、0になる。ちなみに5で割ると4、15で割ると9になった。
なんじゃこりゃ。。DateやTimeに対しても、余り算ができてしまう。
ということで、IsNumericで数値でないものをERRORとするように変更。
また、小数や文字列もパスするように本体を修正すると、こうなった。
Function fnFizzBuzz(x) As String Dim ret As String If IsNumeric(x) Then If x <= 2147483647 And x >= -2147483648# Then If Int(x) = CDbl(x) Then Select Case 0 Case x Mod 3 + x Mod 5 ret = "FizzBuzz" Case x Mod 3 ret = "Fizz" Case x Mod 5 ret = "Buzz" Case Else ret = CStr(x) End Select Else ret = "ERROR" End If Else ret = "ERROR" End If Else ret = "ERROR" End If fnFizzBuzz = ret End Function
色々考慮したためコードがごちゃごちゃしてしまったが、実用に耐えられるプログラムというのはこうしたテストを経て作られるものだ。
そして、テストコードがあると良いのは、リファクタリングが簡単にできること。リファクタリングとは、挙動を変えずにコードを整理することである。コードの書き換えには常に「間違える」というリスクが伴う。テストコードがあれば、すぐに間違いに気づくことができる。
先ほど作成したfnFizzBuzzはあれで完成しているが、Ifのネストが分かりにくいので、禁断の「GoTo」を使って少しスッキリさせてみた。
Function fnFizzBuzz(x) As String Dim ret As String '例外チェック If Not IsNumeric(x) Then GoTo Exception If x > 2147483647 Then GoTo Exception If x < -2147483648# Then GoTo Exception If Int(x) <> CDbl(x) Then GoTo Exception Select Case 0 Case x Mod 3 + x Mod 5 ret = "FizzBuzz" Case x Mod 3 ret = "Fizz" Case x Mod 5 ret = "Buzz" Case Else ret = CStr(x) End Select GoTo Fin '正常終了 Exception: ret = "ERROR" Fin: fnFizzBuzz = ret End Function
このような場合でも、先ほどと全く同じテストコードを用いて検証することができる。
今回は実行条件である「If x <= 2147483647 And x >= -2147483648# Then」を、不実行の条件である以下のコードに書き換えている。
If x > 2147483647 Then GoTo Exception If x < -2147483648# Then GoTo Exception
不実行なので不等号の向きが逆になり、イコールは外れる。
しかしこれは特に間違えやすいポイントで、イコールを付けたままにしたり、向きを変え忘れたり、Notを付けたにも関わらず向きを変えてしまったりという間違いがよく起こるのだ。
このような間違いは、先ほどのテストコードを実行すればすぐに判明する。
堅牢なコードを書くうえで、テストコードを書いておくことは非常に有用である。
ただ、テストを書くのは結構面倒くさい。今回は説明にちょうど良い規模だったのでFizzBuzzを題材としたが、FizzBuzzのような人畜無害なプログラムならテストなんて作らなくても、冒頭で紹介した以下のSubプロシージャで十分だと思う。
Sub FizzBuzz() For i = 1 To 100 Select Case 0 Case i Mod 3 + i Mod 5 Debug.Print "FizzBuzz" Case i Mod 3 Debug.Print "Fizz" Case i Mod 5 Debug.Print "Buzz" Case Else Debug.Print i End Select Next End Sub
逆に、お金が絡むような処理、セキュリティに関わる処理など、ビジネスにクリティカルな影響を与える部分は必ずテストを書いておこう。