JUnit 実践講座 - プログラミングスタイルガイド
JUnit 実践講座 -
プログラミングスタイルガイド
目次
- はじめに
- 実装コードとテストコードの書き方は違う
- テストコードで一般解を扱わないこと
- コメントについて
- リファクタリングについて
- メソッド名と本体について
- テストコードの構成
- Footnotes
- 更新履歴
はじめに
XP による開発全体にいえることですが,JUnit を使った開発では,プログラマは次の 2種類のコードを書かなくてはなりません.
- 実装コード
- 実際にソフトウェアとして実装されるコード
- テストコード
- JUnit を使って書かれるコード
僕の経験では,一般に実装コードよりもテストコードの方がコード量が多く,コードを書くのに費やされる時間もテストコードの方が長くなります.したがって,何も考えずにテストコードを書いていけば,開発の後段階でテストコードが肥大化し,メンテナンスの悪夢に悩まされることになるでしょう.これでは「変更コストカーブが増えない」とする XP の原理に反することになりかねません.テストコードを書くには,なんらかのプログラミングスタイルを確立する必要があります.ここでは,そのプログラミングスタイルについて僕の考えを述べましょう.
実装コードとテストコードの書き方は違う
実装コードとテストコードは根本的に書き方が違います.まずこのことを理解しましょう.テストコードには,次の 2つの意味があります.
- 回帰テストの自動化
- 仕様書およびサンプルコード
回帰テストとは,コードを修正したときに新しくバグが混入していないか,元の仕様を破っていないかテストすることです.テストコードでこれを自動的に検証させます.XP には実装とテストを細かく何度も繰り返す test a little, code a little というプログラミングスタイルがありますが,これによってテストコードが何度も実行され,実装コードの信頼性が増し,プログラマがリファクタリングを行う勇気を与えてくれます.
テストコードにはこれ以外にも重要な役割があります.それは,仕様書およびサンプルコードとしての役割です.テストファーストプログラミングでは,実装コードより先にテストコードを書きます.こうすることで,これから書き始める実装コードの仕様をはっきりさせることができます.また,それ以外にもコードを利用しようというユーザ側にもサンプルコードを提供します.あるクラスを利用したいとき,プログラマはそのクラスの実装コードではなく,そのクラスのテストコードから仕様を確認し,コピー&ペーストしてコードを利用します.
この2番目の 仕様書およびサンプルコードとしての側面は,後から機能追加で新しくテストコードを追加したり,バグや仕様変更でテストコードを修正する際にも重要になります.僕が行った開発では,ある程度開発が進んだ段階で,テストケースのどの部分にテストコードを追加していいのか,どの部分を修正したらいいのかさっぱりわからないコードを大量に作ってしまいました.そうなった原因は,実装コードと同じようにテストコードを書いていったからだと思います.つまり,
実装コードと同じようにテストコードを書けば,将来テストを追加したり修正することが難しくなってしまう
のです.また,実装コードと違って
テストコードには,それ自身をテストするコードが存在しない
という事実もメンテナンスを難しくしている原因でしょう.テストコードのテストコードを作るわけにはいかないのです.後で詳しく説明しますが,僕が考える実装コードとテストコードの違いを簡単にまとめておきましょう.
トピック 方針 コメントについて 実装コードは,コメントを書くのを極力さける. テストコードは, javadoc 等積極的にコメントを書く. リファクタリングについて 実装コードは,リファクタリングを最大限行う テストコードは,多少冗長なコードがあっても読みやすさを最優先 メソッド名と本体について 実装コードのメソッド名は重要 テストコードのメソッド名はそれほど重要でない.メソッド本体が重要.
このように,実装コードとテストコードの書き方は対照的です.
テストコードで一般解を扱わないこと
なぜテストコードと実装コードの書き方が違うのかを考えた場合,それぞれのコードでは書く内容に違いがあることに気づきます.
- 実装コード
- 問題領域の「一般解」を扱う
- テストコード
- 問題領域の「特殊解」を扱う
「一般解」と「特殊解」という用語は,数学の微分方程式論に登場する言葉ですが,ここで説明のため「商品の値段から消費税を求めよ」という問題が与えられたと仮定しましょう.
実装コードでは,この問題を満たす「すべての解」を用意しなければなりません.つまり任意の商品について,消費税を求める必要があるのです.このため消費税を求める式を実装コードとして表します.これがこの問題の「一般解」となります.
しかし,その「すべての解」が正しいかどうかは,一つ一つ「特定の解」を使って検証するしかないのです. 100円の商品なら 5円の消費税,1000円の商品なら 50円の消費税というふうに一つ一つ事例を挙げて検証します.この具体的な事例一つ一つがこの問題の「特殊解」となります.この問題では,もし端数が出たら消費税を円単位に切り上げするのか,切り下げするのか,ということも特殊解として考慮する必要がありますね.
ここで重要なのは,
テストコードで一般解を扱おうとしてはいけない
ということです.任意の解でちゃんと動くかどうかテストしようと思えば,すべての状況をテストコードで考慮することになり,最終的に実装コードと等価になってしまいます.消費税を求める式が書かれたテストコードを使い,同じく消費税を求める式が書かれた実装コードをテストする…これでは何のテストしているのかわかりません.テストコードですべてのテストを行うことはできません.問題の特殊解を用意して検証していくしかないし,そうすべきなのです.JUnit のテストケースでは,実装コードと同じようにリファクタリングや抽象化,一般化を行ってはいけません.それが実装コードとテストコードの書き方の違いになるのです.
ただし,抽象化や一般化を行っていけないのは,問題領域そのものに対してだけです.テストするということそれ自体や,問題領域と直接関係のないプラットフォーム部分に関しては抽象化を行ってかまいません.例えば, JUnit 自体がテストをすることの抽象化になっています.テストケースは TestCase クラスで表されますし,テストの準備と後片付けは setUp, tearDown というメソッドで抽象化されています.また,(僕は使ったことがないのですが)HttpUnit では HTTP の部分に関して抽象化を行いテストしやすくしているはずです.
まずいテストコードでよく見られるのは,問題領域について抽象化を行ってしまい,細かいメソッドの数が増えてテストコードが仕様書やサンプルコードとして見えてこない類のものです.こういったコードは,わかりにくくメンテナンスしにくいテストコードを生み出すことになります.くれぐれもこうならないよう注意しましょう.
コメントについて
ここでは,実装コードとテストコードのコメントの書き方について解説します.まず,最初に言っておきたいのは
実装コードにはコメントを書くべきではない.
ということです.できることなら javadoc も書かないほうがよいのです.コメントを書けば,リファクタリングの度にコメントを更新することになります.これは小さいことに見えて,後から大きな問題になりえます.リファクタリングは,なにも小規模なものだけではありません.コード全体に関わる大規模なリファクタリングもあるし,開発していけば,必ずそういうものに遭遇するでしょう.XP では実装コードはかなり流動的です.常に変わるものです.コメントを書けば,リファクタリングするパワーが半減します.コメントを書く余力があるなら,そのパワーをテストコードに使ってください.
実装コードにコメントを書くのは,次の 2つの場合だけだと思います.まず,javadoc を API ドキュメントとして残しておく必要がある場合.もし納品物件として最終的に javadoc を書かなければならないとき,その作業は開発工程のできるだけ一番最後に行います.もちろん,きちんと工数をとって計画的に行うようにします.それよりもリファクタリング優先,動くコードを書くことが優先です.次に,コードを見てもわからない部分がある場合.例えば,ある特定の Java 実行環境で生じるバグ回避コードがあった場合,その部分にはコメントをいれるべきでしょう.こういった場合はその場できちんとコメントを書いておきましょう.
一方,
テストコードにはコメントを書くべきです.
すでに述べたように,テストコードのテストコードは存在しません.そのため,もしコメントがないと後になって何のテストをしていたのか全くわからなくなるときがあります.必要なテストなのか,冗長で無駄なテストなのか,それすら判断できない場合があります.簡単なものなら,テストの意味を反映させるようテストメソッド名を工夫してもいいでしょう.しかし,それだけでは不十分な場合も多いのです.ある時点で生じたクラスのバグのためのテストや特殊なテストデータを使ったテストなど,テストの種類が増えていけば,非常にわかりにくくなる場合があります.したがって,このテストは何のテストで,そもそも何のために必要か,そういうことがわかるよう javadoc 等でコメントを書いておきましょう.
ここまで読んで矛盾を感じている読者も多いのではないでしょうか? 実装コードは,修正が困難になるからコメントを書くな,とする一方でテストコードにはコメントを書けというのはどういうことかと.また,テストコードもコードである以上,実装コードと同じようなリファクタリングがありコメントがあればその妨げになるはずだ,と思われたかもしれません.しかしテストコードには,仕様書,サンプルコードとしての役割があるのです.このことを重視すれば,コメントを書くのはしかたがないと言えます.テストコードは可読性,参照のしやすさを優先すべきなのです.
リファクタリングについて
リファクタリングは,テストコードに限らず常に行うべきだ,という意見をよく聞きます.しかし,テストコードのテストコードは存在しないし,テストコードには仕様書とサンプルコードとしての役割があるため,
テストコードではリファクタリングを行うべきではない
のです.共通部分があるからといってまとめてメソッドにしてしまうと,特殊解,つまりテストデータが見えにくくなり,テストデータを修正することが困難になります.
テストコードにはテストデータが書かれるため,コード上はマジックナンバーが散乱しているように見えます.実装コードなら,この種のコードを見ると "Replace Magic Number with Symbolic Constant" のリファクタリング(マジックナンバーを定数宣言に変えること)を行いたくなるでしょう.しかし,これはテストコードには当てはまりません.つまり,
テストコードでは,マジックナンバーを直接書く
必要があるのです.テストコードは,なるべくその個所だけを見てテストの意味がわかるようにしなければなりません.テストコードでは実際の値が重要なのです.例えば,プログラム中重力定数を使う必要がある場合,実装コードでは, GRAVITATIONAL_CONSTANT という定数宣言をしてそれを使うべきでしょう.しかし,テストコードでは, 9.81 というマジックナンバーをそのまま書くのです [1].以下は Fowler の Refactering の本に出てくる例をテストコードとしてもじったものですが,
public void testPotentialEnergy() { assertEquals(GRAVITATIONAL_CONSTANT, potentialEnergy(TEST_MASS, TEST_HEIGHT)); } static final double GRAVITATIONAL_CONSTANT = 9.81; static final double TEST_MASS = 1.0; static final double TEST_HEIGHT = 1.0;
よりも
public void testPotentialEnergy() { assertEquals(9.81, potentialEnergy(1.0, 1.0)); }
のほうがテストコードとして優れています.定数宣言をすれば問題領域の抽象化を行っていることになり,メンテナンスしにくいテストコードになってしまいます.さらに次のルールを必ず守っておいたほうがいいでしょう.すなわち,
テストクラスやテストメソッドを他のテストコードから再利用しない
ということです(ここでテストクラスとは TestCase のサブクラス, テストメソッドとは名前が test で始まる JUnit で規定されたテストメソッドのこと).もしこれを行えば,テストコードの見通しが悪くなり後で非常に困ることになります.仕様書・サンプルコードとして機能させるために,テストコードはできる限りその部分だけを見て理解できるように書く必要があります.実際に僕の開発チームでは,開発の半ばでこの事実に気づき,テストコードに対してリファクタリングと正反対のことを行っていました.
しかし,ここでまた矛盾に直面することになります.リファクタリングを行わないということは,それだけ冗長なコードを増やすことになります.これでは実装コードのインターフェイスが変わった場合,テストコードのあちこちを修正することになるでしょう.このことは同様にメンテナンス性の悪さを生み出すことになります.どうすればいいのでしょうか?
これについては,
テストコードでは,問題領域に直接関係のない部分に対して抽象化・リファクタリングを行う
という方針を立てるのです.例えば TestCase クラスのメソッドを
メソッド名 意味 setUp テストの下準備 tearDown テストの後片付け scenario テスト対象のクラスを使ったシナリオ verify 実行結果の検証
というふうに切り出し,各テストメソッド間で共有させます.setUp, tearDown は JUnit に最初から与えられているメソッドですが, scenario, verify メソッドは僕が勧めるメソッドの切り出し方です.シナリオベースのテストケースの書き方 で説明します.
ただ,現実問題としてこれだけでは冗長なコードをうまく減らすことはできません.そこで,
テスト対象のクラスに関する問題領域に直接関係ないところ [2] では,抽象化・リファクタリングを行ってよい
という方針を立てます.ある程度冗長なコードがテストコードに出てきてしまうのはしょうがありません.それよりも読みやすさを優先させましょう.これは,テストコードが特殊解を扱うと同時に,仕様書,サンプルコードとして機能するという性質から来ているのだと思います.
メソッド名と本体について
ここでは,メソッド名と本体について述べましょう.この点についてもテストコードと実装コードは対照的です.本来,オブジェクト指向プログラミングで一番重要なことは「いい名前をつけること」です.クラス名,メソッド名,これらにどれだけいい名前をつけることができたかによって,そのクラスがうまく設計されたかどうかが決まります.極端な話,メソッドの中身よりメソッド名の方が重要だといえるでしょう.
しかし,テストコードの場合,クラス名やメソッド名を考えるのに時間を割く必要はほとんどありません.テストクラス名については,どのクラスのテストを行っているのかが分かれば十分です.僕のやり方は,テスト対象となるクラス名の最後に Test をつけ,実装側とテスト側のクラスをほぼ一対一に対応させる,というものです.例えばこんな感じになります.
実装側 テスト側 Foo クラス FooTest クラス Bar クラス BarTest クラス Baz クラス BazTest クラス
次に,テストメソッド名がそれほど重要ではない理由は,リファクタリングの節で書いたようにそのテストメソッドが他から呼ばれることがないためです.もちろん JUnit の TestRunner からは呼ばれますが,基本的にJUnit が見ているのは public void testXXXX() というシグニチャだけです.XXXX の部分は何でもかまいません.一意性を気にしつつ,もっともらしい名前をつければよいのです.ただしテストメソッドの役割はきちんとコメントに書いておきましょう.
また,Java では throws 句によってそのメソッドがどんな例外を投げるかを書く必要がありますが,これもテストメソッドでは悩む必要がありません.無条件に
public void testXXXX() throws Exception { .... }
とすればよいのです.こう書けば,たとえテストメソッド内で Checked Exception を投げるメソッドを呼んだとしてもコンパイルエラーになりません.例外が投げられればちゃんと JUnit が拾ってくれます.むしろ全部のテストメソッドについて throws Exception を書きましょう.もちろん,このようなコーディングを実装コードで行ってはいけません.
逆に,テストメソッドの中身は注意深く書く必要があります.そのメソッドだけを見て何のテストかがすぐわかるような,できれば制御構造が単純で上から下へシーケンシャルに書かれたテストが好ましいのです. つまり, if 文や while 文, for 文などをあまり使わないようにします.次のコード
for (int i = 0; i < 3; ++i) { assertEquals(new Integer(i), aVector.elementAt(i)); }
よりも,for 文を使わない
assertEquals(new Integer(0), aVector.elementAt(0)); assertEquals(new Integer(1), aVector.elementAt(1)); assertEquals(new Integer(2), aVector.elementAt(2));
のほうが冗長だけれど,いいスタイルなのです(ただし,上のテストコードはもっとうまく書くことができます.オブジェクトの文字列表現を活用しよう で解説します).
テストコードの構成
最後に,テストコードの構成について書いておきましょう.僕は,以下のルールにしたがってテストコードを構成しています.
- テストクラスは,テスト対象クラスに対して 1つだけ用意する.例えば Foo クラスのテストケースは FooTest とネーミングする.
- あるディレクトリ以下のすべてのテストケースを実行するクラスは,AllTests クラスとする.
- それ以外のテストのためだけに利用されるクラスは, t というプレフィックスをつける.例えば tBar クラスなどとする.
- テストクラスのパッケージはテスト対象クラスと同じにする.
- テストクラスのソースファイルもテスト対象クラスのソースファイルと同じディレクトリにおく.
1番目のルールは,"メソッド名と本体について"の節で述べた通りです.2番目のルールは,複数の TestCase インスタンスをまとめる TestSuite クラスを利用してテストを一気に行なうクラスの名前のつけ方です.JUnit を使った開発では,このクラスを使って全体のテストを行ないます.3番目は,テストのためだけのクラスについてのネーミングルールです.リファクタリングの節で述べたように,テストコードではテストそのものの抽象化やプラットフォームについてリファクタリングを行い,コードを共有させることができます.そうして作られたクラスをテストのためだけのクラスと呼んでいます.こういうふうに名前をつけると,リリース時に Ant で簡単に分離できます.
4番目と5番目はパッケージ構成についてのルールですが,これは意見が分かれるところでしょう.テストコードは実装コードとは別のパッケージに分けるべきだとか,テストコードにパッケージスコープのアクセス権を持たせるため,パッケージは同じにしても別ディレクトリに置くべきだという意見もあるのですが,僕にはなぜそうするのか理由がよくわかりません.テストクラスはテスト対象クラスと密接な関係にあるため,物理的にもすぐ近くに置いたほうがよいと思います.もしリリースする時に分離する必要があるのなら, Ant を使って簡単に分離することができます [3] .
Ant の話が出てきたので,Ant を使って実装コードとテストコードを分離する書き方を少しだけ書いておきましょう.上記のネーミングルールに従えば,Ant の fileset エレメントを次のようにすれば実装コードのみを取り出せます.
<fileset dir="."> <include name="**/*.java"/> <exclude name="**/*Test.java"/> <exclude name="**/AllTests.java"/> <exclude name="**/t*.java"/> </fileset>
この fileset エレメントでカレントディレクトリ以下の実装コード( java ファイル)を指定していることになります.
Footnotes
[1] - もっとも,すぐそばで GRAVITATIONAL_CONSTANT を定数宣言しているのなら問題ありません.しかし,テストコードの特殊解は微妙な値をとり,名前が付けられなくなることがあります.その場合はコメントで補うようにしましょう.
[2] - 「問題領域に直接関係ないところ」の意味ですが,次のように解釈してください.今実装しようとしているクラス(テストされるクラス)には,もともとこういう機能がほしいから作るんだという目的があります. ここでは,その目的に直接関連した部分を「問題領域」と呼んでいます.例えば,商品を表す「商品クラス」を考えてみます.このクラスは,データベースの商品マスタ表に格納されているレコードからデータを抽出し,オブジェクトとして表したものだとします.すると,この商品クラスのテストコードを書くためには,次の2つのコードが混在することになるでしょう.
- 商品としてのふるまいをテストするところ
- RDB に直接アクセスして商品マスタを操作するところ
1番目は,商品クラス本来の問題領域に関するテストです.一方,2番目のRDBにアクセスするところは,商品クラスに直接関係のない部分になります.2番目のようなところを「問題領域に直接関係のないところ」と呼びました.この部分は,テストコードから抽象化して隠蔽してしまったほうがテストコードの目的がはっきりします.
[3] - これは,開発環境に依存するかもしれません.IDE を使っている人は別にしたほうがいいかもしれない,と最近は考えています.テストコードと実装コードを交互にいじっているので,それがストレスなくできる環境なら別フォルダでもいいでしょう.僕は昔ながらの Makefile を使った開発をしているので,同じディレクトリにあるほうが楽なのです.
更新履歴
Footenotes を追加しました - 2002/04/01