堅牢なソフトウェアを作ろうと思ったら、テストは欠かせないプロセスだ。
ソフトウェアテストというのは、要するに「ちゃんと動くかどうか」を確認する作業なのだが、「使ってみました。たぶん問題ありません。」という簡単な話でもない。
私もあまりテストに関して知識が無かったので、以下の書籍を読んだ。
語り口が軽快でサクサク読めるので、テストに興味があるけれどまったく初めてという人にお勧め。
さて、この本で学んだ知識(同値分割法・境界値分析法)を基にFizzBuzzのテストを書いてみようと思う。
まずは、普通のFizzBuzzコード
Sub FizzBuzz()For i =1To100SelectCase0Case i Mod3+ i Mod5
Debug.Print"FizzBuzz"Case i Mod3
Debug.Print"Fizz"Case i Mod5
Debug.Print"Buzz"CaseElse
Debug.Print i
EndSelectNextEndSub
問題なく動作するが、戻り値が無いのでテストができない。
これをテスタブルにするためには、Functionプロシージャで関数化すれば良い。
Function fnFizzBuzz(x)AsStringDim ret AsStringSelectCase0Case x Mod3+ x Mod5
ret ="FizzBuzz"Case x Mod3
ret ="Fizz"Case x Mod5
ret ="Buzz"CaseElse
ret =CStr(x)EndSelect
fnFizzBuzz = ret
EndFunctionSub TestableFizzBuzz()For i =1To100
Debug.Print fnFizzBuzz(i)NextEndSub
すると、fnFizzBuzz関数のテストを書くことができる。
まずは簡単なテストから。
Sub TestfnFizzBuzz1()
Debug.Assert fnFizzBuzz(6)="Fizz"
Debug.Assert fnFizzBuzz(10)="Buzz"
Debug.Assert fnFizzBuzz(30)="FizzBuzz"
Debug.Assert fnFizzBuzz(16)="16"MsgBox"テスト完了"EndSub
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"テスト完了"EndSub
これもパス。
次に、負数を与えてみたらどうなるのか。
数学的には、負数の余り算も成り立つので、パスしてくれないと困る。
マイナスゼロなんてのは無いけれど、一応いれてみた。
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"テスト完了"EndSub
これも無事にテストパス。
次にLong型の最大値境界と最小値境界をテスト
Sub TestfnFizzBuzz4()
Debug.Assert fnFizzBuzz(2147483647)="2147483647"
Debug.Assert fnFizzBuzz(2147483648#)="2147483648"
Debug.Assert fnFizzBuzz(-2147483648#)="-2147483648"
Debug.Assert fnFizzBuzz(-2147483649#)="-2147483649"MsgBox"テスト完了"EndSub
おっと、ここでオーバーフローエラー
![f:id:t-hom:20160228170209p:plain f:id:t-hom:20160228170209p:plain]()
![f:id:t-hom:20160228170258p:plain f:id:t-hom:20160228170258p:plain]()
つまりfnFizzBuzzは、Long型の最大値2147483647を超えるとバグが発生する関数ということ。
ということで、関数本体を以下のように修正。
境界値を超えると"ERROR"という文字列を返すようにした。
Function fnFizzBuzz(x)AsStringDim ret AsStringIf x <=2147483647And x >=-2147483648# ThenSelectCase0Case x Mod3+ x Mod5
ret ="FizzBuzz"Case x Mod3
ret ="Fizz"Case x Mod5
ret ="Buzz"CaseElse
ret =CStr(x)EndSelectElse
ret ="ERROR"EndIf
fnFizzBuzz = ret
EndFunction
テストも以下のように修正する。
Sub TestfnFizzBuzz5()
Debug.Assert fnFizzBuzz(2147483647)="2147483647"
Debug.Assert fnFizzBuzz(2147483648#)="ERROR"
Debug.Assert fnFizzBuzz(-2147483648#)="-2147483648"
Debug.Assert fnFizzBuzz(-2147483649#)="ERROR"MsgBox"テスト完了"EndSub
これでテストは無事にパス。
次に、悪いデータのテストを行う。
悪いデータとは、本来想定されていないデータのことで、たとえば文字列、日付、小数などを受け取ったときにコードがどう振る舞うかをテストする。
出来上がった最終のテストコードはこちら。
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"
Debug.Assert fnFizzBuzz(2147483647)="2147483647"
Debug.Assert fnFizzBuzz(2147483648#)="ERROR"
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"テスト完了"EndSub
一旦本体はそのままでテストしてみると、Nowを渡したときにコードが中断した。
![f:id:t-hom:20160228171432p:plain f:id:t-hom:20160228171432p:plain]()
フム。。
"ERROR"を返すコードは書いてないが、Nowを渡すと例外が発生すると思っていた。
しかし、Now Mod 3とすると、0になる。ちなみに5で割ると4、15で割ると9になった。
なんじゃこりゃ。。DateやTimeに対しても、余り算ができてしまう。
ということで、IsNumericで数値でないものをERRORとするように変更。
また、小数や文字列もパスするように本体を修正すると、こうなった。
Function fnFizzBuzz(x)AsStringDim ret AsStringIfIsNumeric(x)ThenIf x <=2147483647And x >=-2147483648# ThenIfInt(x)=CDbl(x)ThenSelectCase0Case x Mod3+ x Mod5
ret ="FizzBuzz"Case x Mod3
ret ="Fizz"Case x Mod5
ret ="Buzz"CaseElse
ret =CStr(x)EndSelectElse
ret ="ERROR"EndIfElse
ret ="ERROR"EndIfElse
ret ="ERROR"EndIf
fnFizzBuzz = ret
EndFunction
色々考慮したためコードがごちゃごちゃしてしまったが、実用に耐えられるプログラムというのはこうしたテストを経て作られるものだ。
そして、テストコードがあると良いのは、リファクタリングが簡単にできること。リファクタリングとは、挙動を変えずにコードを整理することである。コードの書き換えには常に「間違える」というリスクが伴う。テストコードがあれば、すぐに間違いに気づくことができる。
先ほど作成したfnFizzBuzzはあれで完成しているが、Ifのネストが分かりにくいので、禁断の「GoTo」を使って少しスッキリさせてみた。
Function fnFizzBuzz(x)AsStringDim ret AsStringIfNotIsNumeric(x)ThenGoTo Exception
If x >2147483647ThenGoTo Exception
If x <-2147483648# ThenGoTo Exception
IfInt(x)<>CDbl(x)ThenGoTo Exception
SelectCase0Case x Mod3+ x Mod5
ret ="FizzBuzz"Case x Mod3
ret ="Fizz"Case x Mod5
ret ="Buzz"CaseElse
ret =CStr(x)EndSelectGoTo Fin
Exception:
ret ="ERROR"
Fin:
fnFizzBuzz = ret
EndFunction
このような場合でも、先ほどと全く同じテストコードを用いて検証することができる。
今回は実行条件である「If x <= 2147483647 And x >= -2147483648# Then」を、不実行の条件である以下のコードに書き換えている。
If x >2147483647ThenGoTo Exception
If x <-2147483648# ThenGoTo Exception
不実行なので不等号の向きが逆になり、イコールは外れる。
しかしこれは特に間違えやすいポイントで、イコールを付けたままにしたり、向きを変え忘れたり、Notを付けたにも関わらず向きを変えてしまったりという間違いがよく起こるのだ。
このような間違いは、先ほどのテストコードを実行すればすぐに判明する。
堅牢なコードを書くうえで、テストコードを書いておくことは非常に有用である。
ただ、テストを書くのは結構面倒くさい。今回は説明にちょうど良い規模だったのでFizzBuzzを題材としたが、FizzBuzzのような人畜無害なプログラムならテストなんて作らなくても、冒頭で紹介した以下のSubプロシージャで十分だと思う。
Sub FizzBuzz()For i =1To100SelectCase0Case i Mod3+ i Mod5
Debug.Print"FizzBuzz"Case i Mod3
Debug.Print"Fizz"Case i Mod5
Debug.Print"Buzz"CaseElse
Debug.Print i
EndSelectNextEndSub
逆に、お金が絡むような処理、セキュリティに関わる処理など、ビジネスにクリティカルな影響を与える部分は必ずテストを書いておこう。