テストばっかり書いている。テストコードなんかの綺麗さを追求しても仕方ないかなと思っていたのだけれど、適当に書いていると重複が多くて大変で、シンプルに楽に書くコツみたいなのを掴んできたのでメモしておく。JUnitを対象にする。
テストクラスの単位
ユニットテストは機能要件をテストするものだ。非機能要件(性能、保守性、etc)などはテストできない。ユニットテストを書けるようなコードは保守性も上がるし、テストがあるコードは壊せないのでチューニングもしやすいというのは事実だろうが、これらをユニットテストで確認することはできない。
機能をテストするのだから、JUnitのクラスは機能(function)ごとに分けるのが自然だ。基本的にはクラス・メソッドごとになるかと思う。普通は機能ごとにクラスがあって、そのサブ機能がメソッドになっているものでしょう。したがって、クラスとそのテストのクラスは1対多にするのが綺麗でしょう。
package private, protectedのクラスやメソッドもテストしたいので、テストクラスのパッケージはテスト対象のクラスと同じにした方が良いです。もちろん、同じディレクトリにテストとその対象を入れて管理するのはやめた方が良い。何も考えずにApache Mavenの標準のディレクトリ構成にしたら良い。
テストクラスの命名
メソッドごとにクラスをつくる場合はテストのクラスの命名は、TestClassName_methodName
がいいかと思う。TestClassNameMethodName
でもありだ。この辺は好みの問題だろう。メソッドごとに作ると数が多くなりすぎるなら、TestClassNameXxxOperations
というのもいいかもしれない。しかし、XxxOperations
とくくれるなら別のクラスにしてしまった方が整理される場合も多そうだ。テスト対象のクラスが小さい場合はテストクラスは1個で良いだろう。この場合の名前はTestClassName
になる。
コンストラクタで複雑なことをしているならば、TestClassName_init
かTestClassNameConstructor
とする。ただし、コンストラクタで複雑なことをするのはあまり綺麗なコードではないと思うので、コンストラクタのテストケースが増えそうなら考えなおした方がいいと思う。
また、メソッドをオーバーロードしているなら、それらは同じ機能に決まっているので同じクラスでテストする。別の機能のものに同じ名前が付いているなら、メソッドの名前を変えてください。
テストメソッドの単位
JUnitのテストメソッドというのはつまりテストケースのことです。冗長になってしまうけれども、テスト対象のメソッドを1回呼ぶごとにメソッドを作るのがいいと思う。
AをしてBを確認して、次にCをしてDを確認してみたいな、操作のシーケンスをテストケースにするはやめたほうが良い。何のテストをしたいのか焦点がわからないことと、それ故にテストコードが読みづらくなること、テストケースのサイズが大きくなって書くのに時間がかかることが理由である。経験的に小さいテストを沢山書いたほうが、冗長であっても作業が速い。また、シーケンスのテストケースを作ってしまうと網羅性があるのか分からなくなるだろう。それに、小さいテストでないと、setUp
, tearDown
が上手く使えないことからして、シーケンスのテストはユニットテストの意図から外れている。
ただし、staticメソッドだとかImmutableオブジェクトのような副作用がなくて1行で済むようなテストケースは1つのテストメソッドに並べて書いてよいと思う。
@Test testStaticMethod() { assertEquals(1, max(1, 0)); assertEquals(1, max(0, 1)); assertEquals(1, max(1, 1)); } @Test testImmutableObject() { assertEquals("(;_;)", (new Emostion("sad")).toKaomoji()); assertEquals("(^o^)", (new Emotion("happy")).toKaomoji()); }
状態を作るメソッドのテストと状態を見るメソッドのテストは重複する。setterとgetterのようなメソッドだ。
@Test testSetValue() { c.setValue(1); assertEquals(1, c.getValue()); } @Test testGetValue() { c.setValue(1); assertEquals(1, c.getValue()); }
とテストが重複してしまう。今は諦めて重複したテストを作るしかないという意見だ。setterとgetter程度であれば1個で済むがメソッドがもう少し大きなものになると、個別に複数のテストケースの下でテストするべきだと思う。その際にテスト内容に重複は生じてしまうが、テスト対象のメソッドが異なるのでテストの焦点が別として、同じテストを2回書くのも止むからぬことかと思っている。あえて重複を排除したければ、状態を見るメソッドはテストで何度も呼ばれるので、代表的な動作は省いてよいかもしれない。ただし、コードが出来た後必ずそのルートが通っているかのチェックはすること。
テストメソッドの命名
メソッドの命名はちょっと難しい。あまり悩んでも仕方がないが、重複は言語仕様的に許されないので、適当な名前をつけるしかない。テストケースごとに焦点が決めてていれば、testFocusPoint()
とそれを名前とすればいい。テスト対象のメソッド名がクラス名に入っている場合はそれは不要だろう。似たような名前が多くなるが、特にそれで問題が生じることはない。そのメソッドを呼び出すのは機械だけだからだ。
絶対に避けなければならないのは、testCase1()
, testCase2()
, ...と連番を付けることである。何のテストか分からないこともそうだが、テストケースを増やすときに末尾にしか追加できない。関連あるテストケースはやはり近くに置きたい。テストコードを書く前から全てのテストが分かっていたら苦労はないわけで、追加するときのこととは考えないと破綻する。おすすめはしないが連番付けるよりも4文字ぐらいのランダムな文字列の方が方がマシだと思う。どうしても名前が思いつかなら、メソッドのsuffixに何かしら付ける。1,2,3とかA,B,Cとか。それでもsuffixで区別するのは3種類が限度でしょう。4個以上ならなんでもいいからそれっぽい名前を付けたほうがよい。
テストの流れ
各テストは
- 事前状態を作る
- 操作をする
- 返り値、例外といった出力を評価する
- 事後状態を評価する
の4フェーズから構成される。1,2のバリエーションがテストケースに相当し、3,4は仕様である。契約プログラミングでいうと1,2が事前条件(precondition)の設定で、3,4が事後条件(postcondition)の評価です。副作用がないメソッドならば、1,4はない。
操作はメソッドの呼び出しだ。1つの操作が1つのメソッドです。1つの操作に複数回のメソッド呼び出しが必要なら、例えばmethodA
とmethodB
が必ず順に呼ばれなければならないとかなら、設計を改めたほうが良い。操作のバリエーションは、引数のバリエーションで実現される。オーバーロードの場合はメソッドの種別も操作のバリエーションに相当するだろう。
事前状態・事後状態とは、フィールド値とかDBやファイルシステムの状態のことです。
事後条件の評価は、JUnitではassertXXX
関数で行います。
全てのテストは必ず上記の順に行われる。したがって、コードもその順で書いたほうが良い。事前条件を作った後に、メソッドの引数に渡すオブジェクトを作る。返り値の評価は事後条件の評価の前にする。
また、上記の副作用のない操作以外は、以下のように操作と返り値の評価は同時にすると可読性が下がるのでやめたほうがよい。どれが操作でどれが事後条件の評価なのかが分からなくなる。
assertTrue(object.operation()); // return true if success
assertTrue(object.isGood());
操作の返り値は変数に一旦保存しましょう。
boolean success = object.operation();
assertTrue(success);
assertTrue(object.isGood());
事後条件の評価の共通化
事前条件の作成の共通化より、事後条件の評価の共通化の方がしやすいので先にそれについて述べる。
テストコードを書いているとJUnit組み込みのassertXXX
だけでは足りないことに気づくと思う。その場合は独自のassertXXX
を作りましょう。名前はcheckXXX
でもいいです。ただ評価するだけの関数なので、staticメソッドで作ります。
例えば、ファイル操作をするメソッドのテストをしたいときに、
assertTrue(new File("filename").exists());
と何度も書くぐらいなら、
public static void assertFileExists(String filename) { assertTrue(new File(filename).exists()); }
というメソッドを作って、
asssertFileExists("filename");
とした方が便利です。一行のassertionだけでも可読性と労力が変わります。もっと評価が複雑ならば自前のassertXXXX
を作ったほうがいいです。名前はちゃんと決定した方が良いです。何度も使いまわすのでちゃんと名前を付けておかないと整理しきれなくなる。
独自のassertXXXX
メソッドはクラスごとに出来あがることになる。メソッドは異なっても、評価したいことは同じである場合が多い。そのため、ClassNameTestHelper
とかClassNameAssert
といった名前のクラスを作ってClassName
クラスにまつわるassertionはそこに放り込む。
ちなみに、期待と実際を引数にするときは、JUnitと合わせて、第一引数が期待で第二引数を実際としましょう。
例外の評価
私は以下の自然な書き方がもっとも分かりやすいと思います。
try { object.method(); fail(); } catch (IOException ok) {}
必要に応じて{}
内でmessageを評価したりします。また、副作用がなかったどうかといった評価をこの後に行います。
副作用なんてないに決まっている単純な操作のときは
@Test(expected = IOException.class)
でも良いです。
JUnit4のExpectedException
クラスはあまり便利とは思えない。try-catchと手間は変わらないように思える。
事前条件の設定の共通化
事前条件で操作のバリエーションは引数を変えるだけなので、あまり書くのは大変ではない。しかし、事前状態を作るのは面倒なことも多いので共通化したい。
どの状態から事前条件を作り始めるかは必ず共通化する必要がある。クリーンな状態で各テストは始めたい。この作業はsetUp
で最初に掃除をするか、tearDown
で最後に掃除をするかいづれかの方法を取る。私は前者の方が好きだ。後者を採用する場合は他のテストも同様にゴミを残さないことを期待していることになる。いづれの場合でも、掃除の操作は冪当でなければならない。
テスト対象のクラスの生成はここでします。これは共通の処理なので、テストケース内でnew
を書く必要はない。上で書いたようにImmutableクラスはsetUp
内でnewしても使いまわすことにはならない。
テスト対象のクラスをnewする前提の条件(後述する不変条件)がある場合はsetupの中で作ってもいいですが、毎回作ると時間がかかるので、@BeforeClass
で作ると便利です。ここでする処理は大きめの処理になる。そして、この処理は頻繁に使いまわされるので実装は別ファイルに分ける。また、@AfterClass
で更地に戻すことを忘れないように。@BeforeClass
で作ったものは壊しにくいものが多くなると思うので、壊すのにも専用のルーチンを用意した方が良い。
public class { ClassName object; @BeforeClass public static void setUpBeforeClass { ~~~ } @AfterClass public static void tearDownAfterClass { ~~~ } @Before public void setUp() { object = new ClassName(); ~~~~ } }
あるクラスをテストすると同じような事前条件が多く出ます。そういうのは共通関数化します。デザインパターンの1つであるBuilderパターンを使うとよい。
ClassNameTestHelper
というクラスを作って、staticメソッドのライブラリを作ってBuilderパターンっぽい使い方をするのが1つ。この場合にはClassName
クラスのインスタンスを渡して条件を整えてもらう。メソッドは事前状態のパラメータごとに作って、引数としてパラメータの値を渡すことになる。
public class ClassNameTestHelper { public static void setParameter(ClassName object, Value value) { ~~~ } }
ClassNameStateBuilder
みたいなBuilderクラスそのものをつくるなら、setUpの中でテスト対象のクラスをnewする変わりに、Builderクラスをnewしてフィールドに設定する。そして、テストケース内でそのBuilderクラスを使ってテスト条件を作っていく形となる。Builderクラスの書き方は
が参考になるかと思う。GoF Builderを使うことになると思う。
状態のパラメータはクラスの状態だけではない。クラス以外の状態の方が作るコードを書くのが面倒なので共通化すると威力を発揮する。だから、テスト対象のクラスのBuilderではなくて、テスト環境みたいなちょっと広めの仮想的なクラスのBuilderと思った方が良い。
クラスを継承するとき
以下の例ではコンストラクタを呼ぶ特別なメソッドを作っています。
public class { ClassName object; public ClassName createClassName() { return new ClassName(); } @Before public setUp() { object = createClassName(); ~~~~ } }
ClassName
クラスを継承したExtendedClassName
クラスを作る場合にはこのようにテストを書くと良いかと思う。以下のようにcreateClassName
クラスをoverrideすることでテストを使いまわせる。overrideしてメソッドの動作を変えたところだけ、テストメソッドもoverrideして書き直したり、新たに追加したりすればよい。
public class TestExtendedClassName_methodName extends TestClassName_methodName { @Override public ClassName createClassName() { return new ExtendedClassName(); } }
なお、ClassName
クラスがabstractの場合は、createClassName
もabstractにする。また、テストクラス名もClassNameBaseTests
とかにしておく。
不変条件
メソッドを呼ぶ前に成立していて、かつメソッドを呼んでも変わらないものを不変条件という。不変条件は多すぎるので全てを評価することは出来ない。1 + 1 == 2
が真なことも不変条件だが評価の意味はないし、プロセスが生きているとかも当たり前だ。テスト対象のクラスをnewする条件は不変条件になるが、メソッドを呼んだから条件が満たされなくなることなんてなさそうだ。
なので、あまり評価すべきことは多くないように思うが、あえて評価するべきかなと思うのは、メソッドの副作用を受けないはずのフィールド値が変わっていないことぐらいであろうか。ただ、これはもしかしたら不変条件でないかも。存在することは不変条件だが、変わっていないことは事後条件の1つかもしれない。
でも、そこまではテストする必要はないと思う。やり始めるとキリがないし、リスクも少ないように思う。知らんうちに知らん場所を変えていたなんてオブジェクト指向プログラミングで起こることはまれだろう。そういうことが起こった時だけテストに追加すれば良いように思う。この評価はtearDown()
で行うと共通化できる。
static importを使う
assertXXX
はstatic methodです。しかも頻繁に登場します。したがって、static import
にする。*
にするか全て展開するかは好みで決めてよいがそういうつまらないことほど統一したい。
import static org.junit.Assert.*;
privateメソッドのテスト
priveteMethod
という名前のprivateメソッドをテストしたい場合もあるでしょう。そういう場合はそのメソッドを呼ぶためのメソッドをつくる。
static public Returned callPrivateMethod(Classname cname, Arg1 arg1, Arg2 arg2) { Method method = Classname.class.getDeclaredMethod("privateMethod", Arg1.class, Arg2.class); method.setAccessible(true); return (Returned)method.invoke(cname, arg1, arg2); }
そして、callPrivateMehtod
に対してテストケースを書くという形になる。ただし、privateメソッドは副作用も隠蔽されているはずなので、事後条件の評価も面倒となる。公開されているメソッドのテストを頑張った方が費用対効果は高いので、あんまり頑張ってはいけない。
テストは本当に欲しいものではない
本当に欲しいのはテスト対象のプログラムである。もっというとテスト対象が目的とした課題の解決が本当にしたいことだ。テストをさっさと終わらせたり、保守性を高めるためには、整理されたテストコードの書き方が必要だと思ったので書いている。しかし、整理されたテストコードが欲しいわけではない。整理されたテストコードを作ってウットリしてはいけない。仕様書とかガントチャートとかバグ表とかと同じことですね。