【96】テストは正確に、具体的に

ケブリン・ヘニー(Kevlin Henney)

 先にも書いたとおり、ユニットテストにおいては、実装コードの「偶然の仕様」への合致を確認するのではなく、コードの動きが本来の要求に合っているかを確認することが大切です。だからと言って、それを言い訳にテストが暖昧なものになるようでは因ります。テストはあくまで正確で厳密なものでなくてはなりません。

 ここでは理解しやすいよう、あえて古典的な例をあげて説明してみましょう。ソート処理のコードをテストするという例です。今の時代に、業務の中でソートアルゴリズムの実装を求められるプログラマはあまりいないでしょう。しかし、ソートなら馴染み深く、ともかくどういう処理なのかを誰しもが知っている(少なくとも知っていると信じている)ので都合がいいのです。ただ、馴染みがあるだけに、自分の思い込みに気づきにくいというのも確かですが。

 ソート処理のコードをテストする場合、「テストで何を確認するのか」とプログラマに尋ねたとすると、ほとんどが「ソート処理の結果、シーケンスの要素がソートされたものになっているかを確かめる」という類の返答になります。この答えは確かに正しいのですが、完全に正しい、とは言えません。「処理結果を正しいとみなす条件を、もう少し詳しく教えて欲しい」というと、多くのプログラマから「処理の以前と以後でシーケンスの長さが同じになっていること」という答えが返ってきます。これも正しいのですが、やはり十分に正しいとは言えません。たとえば、以下のようなシーケンスがあったとします。

3 1 4 1 5 9

 これをソートした結果、以下のシーケンスが得られたとしましょう。もし、テスト結果を正しいとみなす条件が「シーケンスがソートされたものになっていること」「処理の以前と以後でシーケンスの長さが同じになっていること」であれば、この結果でも、条件を満たしていることになります。

3 3 3 3 3 3

 いくら条件を満たすとは言っても、これで良いと思う人はまずいないでしょう。この例は、実際の開発プロジェクトで発生したエラー(幸い、製品のリリース前に発見できました)を基にしたものです。コードを書いている時、単純なタイプミスをしたのか、それとも何か勘違いをしたのか、ともかく結果的に配列のすべての要素を先頭の要素と同じにする、というコードを書いてしまっていたのです。

 テスト結果を正しいとみなす条件としては、「結果としてできるシーケンスがソートされたものになっている」に加えて、「シーケンスが、ソート前と同じ値で構成されている」も必要でしよう。これで、正しい動作と正しくない動作を適切に見分けることができます。「シーケンスの長さが元と同じ」という条件は、「同じ値で構成されている」という条件が満たされれば自動的に満たされるので、改めて設ける意味はありません。

 ただし、条件を上のように記述しただけでは、良いテストはできません。良いテストというのは、まず読みやすく、理解しやすいものである必要があります。かつシンプルで、正しいか間違っているかが即わかるものでなくてはなりません。「シーケンスがソートされたものになっていて、ソート前とソート後の値の構成が同じ」ということをチェックするコードがすでにあれば別ですが、そうでない状態でテストコードを書こうとすると、テスト対象のコードより複雑なものになってしまう可能性が非常に高いでしょう。Tony Hoare はそれについて次のように言っています。

ソフトウェアをデザインするには 2 通りの方法がある。1 つは、非常にシンプルにして、一見して欠陥がないとわかるようにするという方法。もう 1 つは、非常に複雑にして、欠陥があっても一見しただけではわからないようにするという方法である。

 テストコードが複雑になると、誤りも起きやすくなります。複雑にならないようにするには、具体的な例を使うという方法が有効です。たとえば、以下のようなシーケンスがあったとします。

3 1 4 1 5 9

 そして、これをソートすると、結果が次のようになるとします。

1 1 3 4 5 9

 テストでは、これ以外の結果をすべて正しくない、とみなすのです。他のどのような結果も無効とします。

 動きについて記述する時、具体的な例があれば、わかりやすく、暖昧さを残さずに記述できるのです。「空のコレクションに 1 つ項目を追加する」という処理の結果を、単に「コレクションが空になっていない」と表現しただけでは不十分です。「コレクションには項目が 1 つあり、それは追加された項目である」と表現する必要があります。こう表現すれば、「コレクションにもし 2 つ以上項目があれば、それはコレクションが空の場合と同様、無効な結果であり、処理が正しくなかったことになる」とわかります。また、「たとえ項目は 1 つでも、それが追加された項目と違っていれば、やはり結果は無効で、処理が正しくなかったことになる」ということもわかります。「テーブルに行を 1 つ追加する」という処理の結果は「テーブルが行 1 つ分、大きくなる」と表現しただけでは不十分です。「増えた行のキーを使用すれば、追加された行が取得できる」ということもつけ加える必要があるでしょう。

 このように、動きについての記述は、ただ正確なだけでは不十分です。それに加え、誤解の余地のないものにしなくてはならないのです。