何度か講演でこの話をしたのだが、気が向いたのでエッセンスを書き下しておこうと思う。
テスト駆動という言葉が流行る前にプログラマとなった私は、当初どのようにテストを書いてよいのか分からなかった。そんなとき、(当時はオーム社で現在はラムダノートの)鹿野さんから「ビューティフルコード」を献本していただいた。分厚い本なので、興味ある章から読んでいった。その一つがアルベルト・サボイア氏が書いた7章「ビューティフル・テスト」だ。
この章では、例として二分探索が取り上げられる。二分探索のアイディアが出されたのは1946年だが、バグのない実装ができたのは12年後だという。実際に実装してみると分かるが、ソートされた配列の中に目的の要素が含まれるのか検査する二分探索のコードを正しく書くことは難しい。格好の題材というわけだ。
テストがよく分かっていない私にとってありがたかったのは、JUnitの設計指針が説明されていたことだ。assertEquals
で単純に比較できるようになっており、この関数を使ってテストできるようにコードを設計しないといけないと分かった。
この章では以下のようなテストが説明される。
スモークテスト
単純な利用例を使ったテストである。(この書籍には書かれてないが)名前の由来は、昔、ハードウェアが完成したら、まず電源を入れて煙が出ないか確かめたことだという。
実際には、次のように書く。(適当な疑似コードなので、雰囲気で理解して欲しい。)
# 対象となる配列 arr = {1, 4, 42, 55, 67, 87, 100, 245} # 42 が入っている位置を確認 assertEquals( 2, binarySearch(arr, 42)) # 43 が入ってないことを確認 assertEquals(-1, binarySearch(arr, 43))
境界テスト
境界テストとは、データ構造の端っこを検査することである。固定長の数値あれば、下限と上限のテストは簡単である。配列のようにいくらでも大きくなれるデータ構造であれば、少なくとも大きさ0や1といった下限をテストすべきである。二分探索では、配列が半分、半分と分割され境界が変わっていくので、境界テストが大切になってくる。
配列の下限のテストは、以下のように書ける。
# 大きさ0の配列 assertEquals(-1, binarySearch({ }, 42)) # 大きさ1の配列 assertEquals( 0, binarySearch({ 42 }, 42)) assertEquals(-1, binarySearch({ 42 }, 43))
以下は、要素の位置に関する境界テストである。
arr = { -324, -3, -1, 0, 42, 99, 101 } # 左端 assertEquals(0, binarySearch(arr, -324)) # 真ん中 assertEquals(3, binarySearch(arr, 0)) # 右端 assertEquals(6, binarySearch(arr, 101))
世間では、こういったテストをCIなどで自動的に走らせることを「テストの自動化」と言っている場合が多い。
ランダムテスト
境界テストを書いていると、いろんなテストデータを人間が用意するのは馬鹿らしいと思うようになる。テストデータは自動生成したい。ランダムテストとは、テストデータを乱数的に生成するテストのことである。
テストデータをうまく生成できれば、コーナーケースを発見しやすくなる。個人的には、ここまでの自動化をやって初めて「テストの自動化」と呼びたい。
突然変異テスト
テストするコードに若干変更を加えて、テストする方法である。若干の変更が加わった突然変異は、元のコードとは若干異なる振る舞いをする。テストが十分であれば、突然変異はテストをすり抜けられない。言い換えると、突然変異がテストをすり抜けたとしたら、テストは十分でないので、テスト項目を増やす必要がある。突然変異テスト対しては深入りしないので、興味があればビューティフルコードを読んで欲しい。
私はビューティフル・テストの章を読んで景色が変わった。テストが書けるようになったのだ。テストのためには、コード側もテストしやすいように作る必要がある。テストのし易さが、私の設計指針に加わったのだ。
性質テスト
Haskellでプログラムを書くようになり、Haskellerの常としてQuickCheckと出会った。QuickCheckとは性質テストのための代表的なテストフレームワークである。コードの性質を書いておくと、テストデータは自動生成される。
乱数テストの一種とも言えるが、テストデータの生成に工夫がある。テストデータは、小さいものから、だんだん大きくなっていくので、下限の境界が網羅される。また、たとえば、木構造に対して深さ優先で生成するのか、幅優先で生成するのかといった戦略がテストフレームワークごとに決まっている。
QuickCheckを使って性質テストを書くようになってからしばらくして、ビューティフル・テストを読み返してみた。以前あれほど感動した内容なのに、「なにをまどろっこしいことをやっているのだ」という感想に変わったのだ。
スモークテスト、境界テスト、ランダムテスト、そして突然変異テストで説明してあることは、計算量を除いて、単に線形探索と同じ振る舞いをするというだけのことだ。つまり、性質テストであれば以下の一行で書ける。(本当だよ。)
# == は単に等しいかを調べる # arr や x は自動生成される linearSearch(arr, x) == binarySearch(arr, x)
なんということだろう。気付かないうちに、また景色が変わっていたのだ。
私はHaskellでTLSライブラリを保守しているが、テストにはQuickCheckを使った性質テストが書かれている。クライアントとサーバに渡すパラメータが自動生成され、共通のパラメータがあればハンドシェイクは成功し、なければ失敗するといった具合だ。
Happy testing!