JUnit 実践講座 - Converterを使ったJUnitプログラミング
JUnit 実践講座 -
Converterを使ったJUnitプログラミング
目次
はじめに
オブジェクトの文字列表現を活用しよう では,オブジェクトを文字列に変換してassertEqualsを使おう,ということを説明しました.ここでは,文字列変換のときに役立つ汎用ライブラリ:Converterクラスライブラリを紹介します.
Converterクラスライブラリを使えば,オブジェクトの様々なデータ構造を文字列で表現できます.以前汎用文字列変換クラスとしてToStringクラスを紹介しましたが,Decoratorパターンを使って大幅に書き換えたものが今回紹介するConverterライブラリです.ToStringよりConverterの方が便利ですのでぜひ使ってみてください.
ダウンロード
下記のstring-converter.zipを解凍してstring-converter.jarをクラスパスに設定します.
バージョン 公開日 ダウンロード 説明 1.2.0 2003/11/05 junit.extensions.converter_1.2.0.zip Readme.txt 1.1 2003/06/15 string-converter.zip Readme.txt 1.0 2003/05/05 string-converter.zip -
ざっとクラス構成を知りたい方は,javadocをご覧下さい(すみません,全くコメントなしです^^;).
Converterを使ってみよう
まずはどんな感じなのか理解してもらうため,オブジェクトの文字列表現を活用しよう で取り上げたサンプルコードをConverterで書き直してみることにしましょう.
サンプルプログラム - Book クラス
次のようなBookクラスを考えましょう.このクラスは,一冊の本の情報を保持するエンティティクラスです.ISBNコードを引数にとってデータベースから本の情報を取り出します.
public class Book { public Book(String isbn) { ... } public String getISBN() { ... } public String getTitle() { ... } public String getAuthor() { ... } public String getPublisher() { ... } public String getPageCount() { ... } public int getPrice() { ... } public String getSize() { ... } }
このときTestCaseであるBookTestクラスは次のように書けます.Bookオブジェクトを文字列に変換することを思い出しながらコードを見てください.
1: import junit.framework.*; 2: import junit.extensions.converter.*; 3: 4: public class BookTest extends TestCase 5: { 6: public BookTest(String name) 7: { 8: super(name); 9: } 10: public void testAgileBook() throws Exception 11: { 12: Converter converter = new DefaultConverter(); 13: converter = new BeanConverter(converter, Book.class, "ISBN", "title", "author"); 14: Book book = new Book("0-13-597444-5"); 15: Object expected = new Object[] { 16: "ISBN=0-13-597444-5", 17: "title=Agile Softare Development", 18: "author=Robert C. Martin", 19: }; 20: 21: converter.assertEquals(expected, book); 22: } 23: }
まず,Converterライブラリを使うためにjunit.extensions.converter.*
をimportします(2行目).そして,テストメソッドの最初にConverterオブジェクトを作り(12行目〜13行目),converter.assertEquals
を使ってBookオブジェクトを検証しています(21行目).
ここで,2つのConverterクラスが登場します.
クラス名 BookTestでの役割 本来の機能 DefaultConverter expectedオブジェクトの文字列化 Object配列,java.util.Collection等を文字列化 BeanConverter Bookオブジェクトの文字列化 特定クラスをgetterメソッドの値を使って文字列化
つまり,Decoratorパターンを使ってexpected(Object配列)とBookオブジェクト両方を文字列に変換できるConverterオブジェクトを作る,というわけです.21行目のconverter.assertEquals
は,JUnitで用意されているassertEqualsメソッドを使った次のコードとほぼ同じです(Conveter.toString
はオブジェクトを文字列に変換するメソッドです).
assertEquals(converter.toString(expected), converter.toString(book));
以上をまとめると,Converterを使ったJUnitプログラミングの手順は次のようになります.
- DefaultConverterオブジェクトを作成する
- 検証するオブジェクトに応じたConverterを作成し,Decoratorパターンでかぶせていく
- 実測値と期待値を計算する
- Converter.assertEqualsメソッドで検証する
このうち,手順1と手順2の一部はsetUpメソッドに入れてしまってもよいでしょう.
展開レベルの設定
さて,上のテストコードはわざと失敗するようなテストにしています.エラーメッセージを見てみましょう(適当に改行を追加).
There was 1 failure: 1) testAgileBook(BookTest)junit.framework.AssertionFailedError: expected:<[ISBN=0-13-597444-5, title=Agile Softare Development, author=Robert C. Martin]> but was:<[ISBN=0-13-597444-5, title=Agile Software Development, author=Robert C. Martin]>
これではエラー箇所がわかりにくいので,21行目のconverter.assertEquals
を展開レベル1にして呼び出すことにします.
converter.assertEquals(expected, book, 1);
こうすると,エラーメッセージは次のように書き変わります.
There was 1 failure: 1) testAgileBook(BookTest)junit.framework.AssertionFailedError: expected:<[ ISBN=0-13-597444-5 title=Agile Softare Development author=Robert C. Martin ]> but was:<[ ISBN=0-13-597444-5 title=Agile Software Development author=Robert C. Martin ]>
Emacsのediff機能を使ってdiffをとってみましょう.エラー箇所が一目瞭然ですね.
展開レベルは,配列またはコレクション型の要素をどこまで展開して表示するかを表しています.配列の中に配列があるようなネストしたデータ構造でも,展開レベルを上げることによってわかりやすく表示することができます.
展開レベルのデフォルト値は0です.この値を変えるには,setExpansionLevelメソッドを用います.
converter.setExpansionLevel(1);
これもsetUpメソッドに入れておいてかまいません.展開レベルを個別に指定したい場合はassertEqualsの第3引数に,一括で指定したい場合はsetExpansionLevelを使ってください.
その他のConverterメソッド
これまでassertEquals, toString, setExpansionLevelの3つのメソッドを紹介しました.他にも知っておくと便利なメソッドがありますので,利用してみてください.
メソッド名 意味 print, println 文字列化されたオブジェクトを標準出力に表示する. assertEqualsIgnoreOrder 配列またはコレクション型のオブジェクトをソートしてから比較する.要素をConverterで文字列化した後でソートするため,各要素がComparableである必要はない.
BeanConverterとその仲間
BookTestクラスでは,Bookオブジェクトの文字列化にBeanConverterを使っていました.このクラスはプロパティ名を指定し,「プロパティ名=プロパティ値」という形式の配列に変換します.いろいろな指定方法があります.
// プロパティ名の引数を並べる(最大5つまで) converter = new BeanConverter(converter, Book.class, "ISBN", "title", ... ); // プロパティ名の配列を使う(6つ以上指定したい場合) converter = new BeanConverter(converter, Book.class, new String[] {"ISBN", "title", ... }); // 無指定(すべてのプロパティを対象にしたい場合) converter = new BeanConverter(converter, Book.class);
ここで少し注意することがあります.プロパティが1つだけ指定された場合,出力結果が配列でなくなってしまうのです.つまり,
converter = new BeanConverter(converter, "title"); convertr.println(new Book("0-13-597444-5"));
とした場合,標準出力には
[title=Agile Software Development]
というサイズ1の配列ではなく
title=Agile Software Development
と出力されます.これは,期待値を用意するときにObject配列のネストが深くなりすぎないようにするための仕様です.
他にも,BeanConverterに似たConverterクラスとして次のようなものが用意されています.いずれもコンストラクタのシグニチャはBeanConverterと同様です.
クラス名 説明 BeanValueConverter クラスのプロパティ名を指定し,プロパティ値の配列に変換する ClassFieldConverter クラスのフィールド名を指定し,「フィールド名=フィールド値」という形式の配列に変換する ClassFieldValueConverter クラスのフィールド名を指定し,フィールド値の配列に変換する
ここで,BeanConverterと同様プロパティ名またはフィールド名が1つだけ指定されたときは配列として変換されないことに注意してください.
さらに,BeanValueConverterとClassFieldValueConverterはフォーマット指定が可能です.例えば,java.awt.Pointオブジェクトを
<point x='10' y='20'/>
というフォーマットで変換したい場合は次のようにします.
public void testPoint() throws Exception { Converter converter = new DefaultConverter(); converter = new ClassFieldValueConverter(converter, "<point x='{0}' y='{1}'/>", Point.class, "x", "y"); converter.println(new Point(10, 20)); }
コンストラクタの第2引数がフォーマット文字列です.{0}, {1}, ... のそれぞれがプロパティ値またはフィールド値に置換されます.
Converterを作ってみよう
これまでに紹介したBeanConverter等を使えば,いろいろなオブジェクトをテストしやすい形に文字列化することができます.しかし,自分でConverterクラスを定義しなければならないときもあります.ここでは,どうやって自分用のConverterを作るかを説明しましょう.
サンプルプログラム - ディレクトリ階層を処理するプログラム
ディレクトリ階層を処理するプログラムのテストコードをConverterで書いてみましょう.このプログラムは,あるディレクトリ以下のファイルになんらかの処理を行うものとします.例えば,ファイルの中身を書き換え,コピーしたり,削除したり,リネームしたりします.ここでは,テストケースのscenarioメソッドでプログラムを動すとtestdir以下に次の3つのファイルができるものとしましょう.
ファイル名 内容 testdir/A.txt This is A.txt testdir/subdir/B.txt This is B.txt testdir/subdir/C.txt This is C.txt
このとき,テストメソッドのおおよそのイメージは次のようになります.
1: public void test() throws Exception 2: { 3: Converter converter = new DefaultConverter(); 4: converter = new DirConverter(converter); 5: 6: scenario(); 7: 8: Object expected = new Object[] { 9: "A.txt=This is A.txt", 10: new Property("subdir", new Object[]{ 11: "B.txt=This is B.txt", 12: "C.txt=This is C.txt", 13: }), 14: }; 15: converter.assertEquals(expected, new File("testdir"), 2); 16: }
まず,DefaultConverterにディレクトリ変換用Converter:DirConverterをかぶせています(3〜4行目).このDirConverterが今回作成するクラスです.次にscenarioメソッドを実行し(6行目),期待値expectedをセットして(8〜14行目),converter.assertEqualsで検証します(15行目).
Propertyクラス
ここで,期待値の設定にPropertyクラスが登場しました(10行目).このクラスは,Converterと同じjunit.extensions.converterパッケージで定義されています.Propertyクラスは「名前=値」という2つのオブジェクトの組を表します.
ところで,大抵の場合オブジェクトのデータ構造は次の3つで表現できるでしょう.
- スカラー
- 配列(またはコレクション)
- プロパティ(「名前=値」の組)
ここで,1番目のスカラーとは配列ではない値のことです.Converterを使ったJUnitプログラミングでは,これを単に文字列として表現することができます.また,2番目の配列はObject配列またはCollectionオブジェクトで表現できます.ところが,3番目のプロパティは適当なものが見当たりません.そこでPropertyクラスの登場になります.
もちろん,プロパティの名前と値がスカラー値なら,
Object expected = "testdir/A.txt=This is A.txt";
という風に=
で区切ればプロパティを文字列で表すことができます(前述のBookTestではこうしていました).でも,名前や値のところに配列がくるとうまく表現できません.もしPropertyクラスを使わないとしたら,
Object expected = new Object[] { "A.txt=This is A.txt", "subdir=[B.txt=This is B.txt, C.txt=This is C.txt]", };
と書くことになりますが,これではConverterの展開レベルを変更したときテストが失敗してしまいます.そこで,プロパティ値が配列になるような場合はPropertyクラスを使ってください.
もちろん,徹底的にPropertyクラスを使って
Object expected = new Object[] { new Property("A.txt", "This is A.txt"), new Property("subdir", new Object[]{ new Property("B.txt", "This is B.txt"), new Property("C.txt", "This is C.txt"), }), };
とすることもできますが,煩雑すぎるでしょう.
さて,Propertyの説明を終えたところで,これまで何度も登場しているDefaultConverterについて補足しておきます.このクラスは,内部で次の4つのConverterを利用しています.
- CoreConverter
- ObjectArrayConverter
- CollectionConverter
- PropertyConverter
CoreConverterは,Decoratorパターンの芯となるConverterです.普段使っているときはDefaultConverterにConverterをかぶせているようですが,実際には一番中心にCoreConverterオブジェクトがいるのです.このクラスは,Object.toString
メソッドで文字列変換を行なっている単純なものです.先ほどのデータ構造の話ではスカラーの変換を担当すると考えてよいでしょう.2番目と3番目のConverterは,配列およびコレクションの変換を担当します.そして,最後のPropertyConverterがプロパティの文字列変換をする,というわけです.
SpecificObjectConverterクラス
次にDirConverterの作成にとりかかりましょう.
特定のオブジェクトに独自の変換機能を追加するには,SpecificObjectConverterクラスを継承します.このクラスのサブクラスでは,次の2つのメソッドをオーバーライドします.
メソッド 意味 DirConverterの場合 boolean canConvertSelf(Object o)
変換できるオブジェクトの条件を表す FileクラスのインスタンスでisDirectoryがtrueのオブジェクト Object convertSelf(Object o)
引数のオブジェクトを別のオブジェクトに変換する ディレクトリ内を走査し,変換を行なう
このことをふまえて,DirConverterをTestCaseの内部クラスで定義してみましょう.
1: private static class DirConverter extends SpecificObjectConverter 2: { 3: public DirConverter(Converter converter) 4: { 5: super(converter); 6: } 7: protected boolean canConvertSelf(Object o) 8: { 9: if (!(o instanceof File)) 10: return false; 11: return ((File)o).isDirectory(); 12: } 13: protected Object convertSelf(Object o) 14: { 15: File[] files = ((File)o).listFiles(); 16: Map map = new TreeMap(); 17: for (int i = 0; i < files.length; ++i) { 18: File file = files[i]; 19: map.put(file.getName(), new Property(file.getName(), file)); 20: } 21: return map.values(); 22: } 23: }
まず,DirConverterはDecoratorなので,コンストラクタは必ずdecorateするConverterオブジェクトを引数にとります(3行目〜6行目).canConvertSelfメソッドでは,オブジェクトがディレクトリかどうかを判定しています(7行目〜12行目).convertSelfメソッドでは,ディレクトリ内のファイル走査し,Propertyクラスのコレクションに変換しています(TreeMapを使っているのは,ファイル名でソートするためです).
convertSelfメソッドを見て疑問を持たれた方はいらっしゃるでしょうか.convertSelfは,再帰呼び出しを使っていないし,ファイルの中身の読み込みも行なっていません.
実は,DirConverterを書いている最中に方針を変更したのです.DirConverterすべてに任せるのではなく,ファイルの中身はFileConverterクラスで処理するほうがいいんじゃないかと.つまり,testメソッドで用意するConverterでは,DirConverterとFileConverterを組み合わせるのです.これがConverterライブラリの便利なところです(と同時にDecoratorパターンの利点でもありますね).
public void test() throws Exception { Converter converter = new DefaultConverter(); converter = new DirConverter(converter); converter = new FileConverter(converter); scenario(); .... }
FileConverterもSpcificObjectDecoratorを使って作成しましょう.
private static class FileConverter extends SpecificObjectConverter { public FileConverter(Converter converter) { super(converter); } protected boolean canConvertSelf(Object o) { if (!(o instanceof File)) return false; return !((File)o).isDirectory(); } protected Object convertSelf(Object o) { StringBuffer buf = new StringBuffer(); int c; try { Reader reader = new BufferedReader(new FileReader((File)o)); while((c = reader.read()) != -1) { buf.append((char)c); } reader.close(); } catch (IOException ex) { buf.append(ex.getMessage()); } return buf.toString(); } }
細かい説明は省きますが,convertSelfメソッドでエラー処理を文字列表現の一部として追加しているところが少し面白いかもしれません.こうしておけばエラー箇所もすぐわかります.
以上でディレクトリ階層のConverterが作成できたので,テストコードが完成しました.
Converter.INVISIBLE定数
さて,その後プログラムの仕様が変わりました.新しい仕様では,ディレクトリを処理した後に拡張子tmpを持つランダムな名前のテンポラリファイルを作成します.しかし,これらのファイルはログとして残しておく必要があり,消すことができません.
テストコードでは,テンポラリファイルは処理内容の検証には無関係なので無視するようにしたいところです.そういった場合はConverter.INVISIBLE
定数を使うとよいでしょう.この定数は,文字列変換で無視するオブジェクトとして定義されています.つまり,拡張子tmpを持つファイルをConverter.INVISIBLEに変換するConverterクラスを書けばよいわけです.
private static class FilterOutTempFileConverter extends SpecificObjectConverter { public FilterOutTempFileConverter(Converter converter) { super(converter); } protected boolean canConvertSelf(Object o) { if (!(o instanceof File)) return false; return ((File)o).getName().endsWith(".tmp"); } protected Object convertSelf(Object o) { return Converter.INVISIBLE; } }
このConverterをテストメソッドでかぶせます.これだけで拡張子tmpを持つファイルやディレクトリを無視することになります.
public void test() throws Exception { Converter converter = new DefaultConverter(); converter = new DirConverter(converter); converter = new FileConverter(converter); converter = new FilterOutTempFileConverter(converter); scenario(); .... }
Converterの優先順位
さきほどのテストメソッドでは,FilterOutTempFileConverterを一番最後にかぶせました.ところが,次のように順序を変えるとテンポラリファイルを無視してくれません.
Converter converter = new DefaultConverter(); converter = new DirConverter(converter); converter = new FilterOutTempFileConverter(converter); converter = new FileConverter(converter);
その理由は,FilterOutTempFileConverterがテンポラリファイルを処理する前にFileConverterが処理してしまうからです.つまりConverterは外側になるほどその処理が優先されます.ご注意ください.
無限再帰呼び出しに注意
ここで,もう一つSpecificObjectDecoratorの注意点を述べておきましょう.このクラスのサブクラスを下手に作ると,スタックオーバーフローになってしまうことがよくあります.例えば,次のようなConverterを考えてみてください.
1: public void testRecursive() throws Exception 2: { 3: Converter converter = new DefaultConverter(); 4: converter = new SpecificObjectConverter(converter) { 5: protected boolean canConvertSelf(Object o) 6: { 7: return o instanceof File; 8: } 9: protected Object convertSelf(Object o) 10: { 11: File file = (File)o; 12: return new Property(file.getName(), o); 13: } 14: }; 15: converter.println(new File(".")); 16: }
convertSelfメソッドの返り値に変換前のオブジェクトが含めています(12行目).こうすればConverterの変換を無限に行なうことになり,スタックオーバーフローになるのです.
ということは,canConvertSelfメソッドを
protected Object convertSelf(Object o) { return o instanceof String; }
としたり,
protected Object convertSelf(Object o) { return true; }
とすれば,無条件で無限再帰になってしまうのでしょうか? ところがそうではありません.今のバージョンのConverterライブラリは,あるタイミングで一旦String型に変換されればとそれ以上は変換しない仕組みにしています.そういった理由から,文字列を別のオブジェクトに変換するConverterを作成してもうまく行かない場合があります.注意してください.
Converterをカスタマイズしよう
最後に,BeanConverter等をもう少し細かくカスタマイズする方法について解説します.
サンプルプログラム - Bookクラス再び
さて,最初に紹介したBookクラスのテストに戻りましょう.実は,このクラスは共著の本に対応していませんでした.そこで,新しくgetAuthorsメソッドを追加することにします.
class Book { ... public String[] getAuthors() { ... } ... }
このとき,テストメソッドは次のように書けます.
public void testDesignPatterns() throws Exception { Converter converter = new DefaultConverter(); converter = new BeanConverter(converter, Book.class, "title", "authors"); Book book = new Book("0-201-63361-2"); Object expected = new Object[] { "title=Design Patterns", new Property("authors", new Object[] { "Erich Gamma", "John Vlissides", "Ralph Johnson", "Richard Helm", }), }; converter.assertEquals(expected, book, 2); }
ところが,getAuthorsメソッドが返す共著者の順が特に決まっていないとしましょう.こういう場合どうすればよいでしょうか.以前,順序が決まっていない配列を比較するときはassertEqualsIgnoreCaseメソッドを使うよう説明しましたが,今回の場合Beanのプロパティなので直接このメソッドを使うことができません.authorsプロパティを簡単にソートする方法はないでしょうか.
SortEvaluatorクラス
こういったときのために,ConverterライブラリにはSortEvaluatorクラスが用意されています.このクラスは,プロパティ名を引数にとってそのプロパティ値をソートしてくれます.SortEvaluatorでBeanConverterをカスタマイズしてみましょう.
import junit.extensions.converter.*; import junit.extensions.converter.evaluator.*; ... public void testDesignPatterns() throws Exception { Converter converter = new DefaultConverter(); converter = new BeanConverter(converter, Book.class, "title", "authors"); converter.addEvaluator(new SortEvaluator("authors")); Book book = new Book("0-201-63361-2"); ... }
このように,addEvaluatorメソッドでSortEvaluatorを追加するだけです(なお,SortEvaluatorはjunit.extensions.converter.evaluator
パッケージにあるため,import文を宣言する必要があります).
SortEvaluatorにはいろいろな指定方法があります.
// プロパティ名の引数を並べる(最大5つまで) converter.addEvaluator(new SortEvaluator("authors", ... )); // プロパティ名の配列を使う(6つ以上指定したい場合) converter.addEvaluator(new SortEvaluator(new String[] {"authors", ... })); // 無指定(すべての配列,またはコレクション型プロパティを対象にしたい場合) converter = new BeanConverter(new SortEvaluator());
その他のEvaluatorクラス
SortEvaluator以外にもEvaluatorクラスがいくつか用意されているので簡単に紹介しておきましょう.いずれもjunit.extensions.converter.evaluator
パッケージで宣言されています.
クラス名 説明 HideKeysEvaluator プロパティ名を引数にとり,そのプロパティを見えなくする HideNullEvaluator プロパティの値がNullのものを見えなくする
EvaluationConverterクラス
すでに紹介した4つのクラス:
- BeanConverter
- BeanValueConverter
- ClassFieldConverter
- ClassFieldValueConverter
は,EvaluationConverterという共通のスーパークラスを持っています.EvaluationConverterという名前は,オブジェクトを様々なkeyで評価(evaluation)する,ということから来ています.BeanConverterとBeanValueConverterの場合はプロパティ名がkeyになりますし,ClassFieldConverterとClassFieldValueConverterの場合はフィールド名がkeyになります.
EvaluationConverterは,与えられたkeyの配列と登録されているEvaluatorでオブジェクトの変換を行います.addEvaluatorはConverterクラスで宣言されていますが,実際にEvaluatorを追加できるのは EvaluationConverterオブジェクトだけです.それ以外のConverterに対してaddEvaluatorは使えないことに注意してください.例えば,
Converter converter = new DefaultConverter(); converter.addEvaluator(new SortEvaluator("authors"));
とするとUnsupportedOperationException例外がスローされます.
Evaluatorクラスも自分で作って拡張することができます.興味のある方はEvaluatorクラスとEvaluationConverterクラスのソースを参照してください.この部分にはAttachmentパターンが使われています.
サンプルプログラム
以上でConverterの説明が一通り終わりました.この記事を書くために書き下ろしたjavaファイルも公開しておきますのでこちらも参考にしてください.
ライセンス
このライブラリのバイナリおよびソースコードはクリエイティブ・コモンズのAttribution Licenseに従います.
This work is licensed under a Creative Commons License.
更新履歴
- 1.2.0にバージョンアップ ― 2003/11/05
- 1.1にバージョンアップ ― 2003/06/15
- ライセンスを明記しました. ― 2003/06/15
- 公開 ― 2003/05/05