!!!JUnit !!概要 面倒なテストを楽にするもの。 何度も同じテストを自動的に行えるので、プログラムの改修を気兼ねなくできる。 !!注意 4.1 から awtやswingの実行画面がなくなった。 *junit.awtui.TestRunner *junit.swingui.TestRunner がなくなり、 *junit.textui.TestRunner だけになった。 これしか使わないのであまり気にしなくてイイ。 http://www.ne.jp/asahi/hishidama/home/tech/java/junit.html 4.1からアノテーションを使えるようになった。 ,  ,JUnit3.8 ,JUnit4.1 ,ライブラリのインポート ,TestCaseクラスをインポートする。 ,Assertクラスの各メソッドをstaticインポートする。Testアノテーション等をインポートする。 ,テストクラスの定義 ,TestCaseを継承する必要がある。 ,特になし。 ,テストメソッドの定義 ,「public void test〜()」testで始まる名前で、public voidで引数なしのメソッドが実行対象。 ,「@Test public void 〜()」@Testアノテーションを付けたpublic voidで引数なしのメソッドが実行対象。 ,テストメソッド実行前後 ,setUp()・tearDown()が各テストメソッドの実行前後に呼ばれる。 ,@Before・@Afterを付けたメソッドが、各テストメソッドの実行前後に呼ばれる。 ,全テストの実行前後 ,なし。 ,@BeforeClass・@AfterClassを付けたメソッドが、全テストの実行前後に呼ばれる。 :3.8の実装例: {{code Java, import junit.framework.TestCase; public class SampleTest extends TestCase { public void testNormal() { 対象クラス obj = new 対象クラス(); int r = obj.試験対象メソッド(); assertEquals(123, r); assertEquals(456, obj.get内部状態()); } public void testException() { try { 試験対象実行(); fail("例外が発生するはず"); } catch(発生すべき例外 e) { // success assertEquals("文言", e.getMessage()); } //試験の続き〜 } } }} :4.1の実装例: {{code Java, import static org.junit.Assert.*; import org.junit.Test; public class SampleTest { @Test public void normal() { 対象クラス obj = new 対象クラス(); int r = obj.試験対象メソッド(); assertEquals(123, r); assertEquals(456, obj.get内部状態()); } @Test(expected=発生すべき例外.class) public void testException() { 試験対象実行(); //文言の確認は無理そう? //別の試験を続けるのは無理そう? } } }} !!準備 http://www.junit.org/ からダウンロードして、 junit-****.jar にクラスパスを通すだけ。 !!サンプル {{code Java, import java.util.*; public class Sample { public static void main(String[] args){ System.out.println("main"); } public int add(int a, int b){ return a+b; } public int divide(int a, int b){ return a/b; } } }} このソースをテストしたい場合 {{code Java, import junit.framework.TestCase; public class TestSample extends TestCase { private Sample sample = null; // 共通の初期化などはここで行う public void setUp(){ // System.out.println("テスト前に呼ばれる"); sample = new Sample(); } public void tearDown(){ // System.out.println("テスト後に呼ばれる"); } public void testMain(){ // System.out.println(sample); } public void testAdd(){ // System.out.println(sample); assertEquals(2,sample.add(1,1)); assertEquals(3,sample.add(1,2)); } public void testDivide(){ // System.out.println(sample); assertEquals(1,sample.divide(1,1)); try { sample.divide(1,0); fail(); } catch (ArithmeticException e){ // ArithmeticException が出たら正常 } catch (Exception e){ fail(); } } } }} 実行は java junit.textui.TestRunner TestSample TestSampleの main からテストを実行することもできる。 JUnit4以降の場合 {{code Java, import junit.framework.Test; import junit.framework.TestSuite; // 色んなテストをしたいとき public class TestSample { public static void main(String[] args){ JUnitCore.main(TestTest.class.getName()); } } }} Junit3以前の場合 {{code Java, import junit.framework.Test; import junit.framework.TestSuite; public class TestSample extends TestCase { public static void main(String[] args){ junit.textui.TestRunner.run(suite()); } public static Test suite() { TestSuite suite= new TestSuite("Sample Test"); suite.addTestSuite(TestSample.class); return suite; } // 略 } }} 実行は java TestSample !!!DBUnit !!概要 DBへのアクセスをJUnitでテストするもの。 元データや結果のデータをエクセルファイルで準備することも出来る。 便利。 !!準備 http://muimi.com/j/test/dbunit/ http://dbunit.sourceforge.net/ からダウンロード。 エクセルを読み書きするには、Jakarta POI が必要。 その他に必要なライブラリは、DBUnit の pom.xml に書かれている。 *slf4j-api-1.6.6.jar *slf4j-log4j12-1.6.6.jar *log4j-1.2.15.jar *poi-3.2-FINAL-20081019.jar *dbunit-2.4.8.jar *junit-4.10.jar あたりがあれば、とりあえず動くはず。 !!サンプル テストの対象となるクラス {{code Java, import java.util.*; import java.sql.*; public class test { public static void main(String[] args){ System.out.println("hoge"); DBAccessor db = DBAccessor.createInstance("org.sqlite.JDBC","jdbc:sqlite:resource/test.db3","",""); new test().writeDB(); db.commit(); } public int add(int a, int b){ return a+b; } public int divide(int a, int b){ return a/b; } public boolean writeDB(){ DBAccessor db = DBAccessor.getInstance(); try { PreparedStatement st = db.prepareStatement("insert into test1 values ('101', 'test100')"); System.out.println("update num = " + st.executeUpdate()); db.close(st); } catch (Exception e){ e.printStackTrace(); return false; } return true; } } }} テストを実施するクラス。 {{code Java, import junit.framework.TestCase; import java.io.*; // データベース関連 import org.dbunit.Assertion; import org.dbunit.database.DatabaseConnection; import org.dbunit.database.IDatabaseConnection; import org.dbunit.dataset.IDataSet; import org.dbunit.dataset.ITable; import org.dbunit.operation.DatabaseOperation; import org.dbunit.dataset.excel.*; public class TestTest extends TestCase { public void setUp(){ System.out.println("テスト前に呼ばれる"); t = new test(); } public void tearDown(){ System.out.println("テスト後に呼ばれる"); } public void testMain(){ System.out.println(t); } private test t = null; public void testAdd(){ System.out.println(t); assertEquals(2,t.add(1,1)); assertEquals(2,t.add(1,2)); } public void testDivide(){ System.out.println(t); assertEquals(1,t.divide(1,1)); // // ただ呼ぶだけでいいとおもう。 // t.divide(1,0); try { assertEquals(0,t.divide(1,0)); fail(); } catch (ArithmeticException e){ } catch (Exception e){ fail(); } } public void testWriteDB() throws Exception { // System.out.println("test2"); DBAccessor db = DBAccessor.createInstance("org.sqlite.JDBC","jdbc:sqlite:resource/test.db3","",""); // // System.out.println("test1"); //準備データをDBに入れる DatabaseOperation.CLEAN_INSERT.execute( new DatabaseConnection(db.getConnection()), new XlsDataSet(new File("resource/export.xls"))); //ロジックの実行 assertEquals(true,t.writeDB()); //DBから実際のデータの取得 IDataSet databaseDataSet = new DatabaseConnection(db.getConnection()).createDataSet(); ITable actualTable = databaseDataSet.getTable("test1"); System.out.println(actualTable.getRowCount()); //期待値データの取得 IDataSet expectedDataSet = new XlsDataSet(new File("resource/writeDB_output.xls")); ITable expectedTable = expectedDataSet.getTable("test1"); //期待値と実際のデータの比較 Assertion.assertEquals(expectedTable, actualTable); } } }} !データを作成する エクセルでデータを作成する。 とりあえず、DBをエクセルにエクスポートしてそれを編集して使う。 エクスポートするために簡単なプログラムを作る。 {{code Java, import java.io.FileOutputStream; import org.dbunit.database.DatabaseConnection; import org.dbunit.dataset.IDataSet; import org.dbunit.dataset.excel.XlsDataSet; public class DataExport { public static void main(String[] args) throws Exception { DBAccessor db = new DBAccessor("org.sqlite.JDBC","jdbc:sqlite:resource/test.db3","",""); DatabaseConnection con = new DatabaseConnection(db.getConnection()); IDataSet dataset = con.createDataSet(); // IDataSet dataset = con.createDataSet(new String[] {"test2"}); XlsDataSet.write(dataset, new FileOutputStream("resource/export_all.xls")); } } }} これで出力されたエクセルを編集して、テストのインプットや結果の比較対象を作成する。 !log4j.properties log4j を内部で使っているので、設定ファイルが無いと怒られる。 log4j.properties をクラスパスが通った場所に置く。 中身はテキトウ。 ### direct log messages to stdout ### log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target=System.out log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %5p %c{1} - %m%n ### direct messages to file mylog.log ### log4j.appender.file=org.apache.log4j.FileAppender log4j.appender.file.File=mylog.log log4j.appender.file.Append=true log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=%d %5p %c{1} - %m%n #log4j.rootLogger=debug, stdout, file # log4j.rootLogger=warn, stdout, file log4j.rootLogger=error, stdout, file 実行は java junit.textui.TestRunner TestSample !!処理日付などの値が必要な場合 テストの結果として、処理日付などが必要な場合は ReplacementDataSet を使って 期待値を書き換えることができる。 http://hamasyou.com/archives/000207 {{code Java, ReplacementDataSet dataSet = new ReplacementDataSet(new FlatXmlDataSet(new FileInputStream("dataset.xml"))); dataSet.addReplacementObject("[SYSDATE]", new Timestamp(System.currentTimeMillis())); }} !!一部のカラムを比較対象から除外する 大きく2つの方法がある。 Assertiont#assertEqualsIgnoreColsを使う方法と DefaultColumnFilterを使う方法 !assertEqualsIgnoreColsを使う Assertion.assertEqualsIgnoreCols(actualTable, expectedTable, new String[] {"update"}); !DefaultColumnFilter を使う ITable filteredActualTable = DefaultColumnFilter.excludeColumn(actualTable, new String[] {"upd_timestamp"}); ITable filteredExpectedTable = DefaultColumnFilter.excludeColumn(expectedTable, new String[] {"upd_timestamp"}); !!結果をソートして比較する SotedTable を使うことでテーブルのデータをソートできる。これを利用して結果をソートして比較できる。 String[] sortColumns = new String[] {"id", "name"} ITable sortedActualTable = new SortedTable(actualTable, sortColumns); ITable sortedExpectedTable = new SortedTable(expectedTable, sortColumns); //期待値と実際のデータの比較 Assertion.assertEquals(sortedExpectedTable, sortedActualTable); !!xlsx をデータとして利用する 列数が256個を超えたりする場合、xlsxを使いたくなるがdbUnitがxlsxに対応してくれていない。 対応するための機能は既にあるので、それを利用する。 !xssfの利用 dbUnitの ・XlsDataSet ・XlsDataSetWriter ・XlsTable をxlsxを使えるようにする。 具体的には org.apache.poi.hssf.usermode を org.apache.poi.ss.usermodel に変更する。 new Workbook は、 WorkbookFactory.create() にする。 !!セルの値にかかわらず、セルを文字列として扱う場合 [[Apache POI]]を参考に XlsDataSetWriter.java をいじる。 XlsDataSetWriter#write {{code Java, public void write(IDataSet dataSet, OutputStream out) throws IOException, DataSetException { logger.debug("write(dataSet={}, out={}) - start", dataSet, out); Workbook workbook = new XSSFWorkbook(); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); this.dateCellStyle = createDateCellStyle(workbook); int index = 0; ITableIterator iterator = dataSet.iterator(); while(iterator.next()) { // create the table i.e. sheet ITable table = iterator.getTable(); ITableMetaData metaData = table.getTableMetaData(); Sheet sheet = workbook.createSheet(metaData.getTableName()); // write table metadata i.e. first row in sheet workbook.setSheetName(index, metaData.getTableName()); Row headerRow = sheet.createRow(0); Column[] columns = metaData.getColumns(); for (int j = 0; j < columns.length; j++) { Column column = columns[j]; Cell cell = headerRow.createCell(j); cell.setCellValue(new XSSFRichTextString(column.getColumnName())); } // write table data for (int j = 0; j < table.getRowCount(); j++) { Row row = sheet.createRow(j + 1); for (int k = 0; k < columns.length; k++) { Column column = columns[k]; Object value = table.getValue(j, column.getColumnName()); if (value != null) { Cell cell = row.createCell(k); DataFormat format = workbook.createDataFormat(); CellStyle style = workbook.createCellStyle(); style.setDataFormat(format.getFormat("text")); cell.setCellType(Cell.CELL_TYPE_STRING); cell.setCellStyle(style); if(value instanceof Date){ // setDateCell(cell, (Date)value, workbook); cell.setCellValue( dateFormat.format(value) ); } else if(value instanceof BigDecimal){ // setNumericCell(cell, (BigDecimal)value, workbook); cell.setCellValue( ((BigDecimal)value).toPlainString() ); } else if(value instanceof Long){ // setDateCell(cell, new Date( ((Long)value).longValue()), workbook); Date dateValue = new Date( ((Long)value).longValue()); cell.setCellValue( dateFormat.format(dateValue) ); } else { cell.setCellValue(new XSSFRichTextString(DataType.asString(value))); } } } } index++; } // write xls document workbook.write(out); out.flush(); } }} !!Assertion#assertEquals 時のエラーを全て表示する方法 標準のままだと、比較するテーブルの差異が複数あっても、1つの差異しか表示してくれない。 普通はそれで問題ない。テストをする上では、差異があるかどうかが重要である。 ところが、テストのついでにデバッグ的なこともしたい場合は、どこがどのように異なっているかを全て知りたい。 そこで、差異をすべて表示するAssertionを作成する。 !DbUnitAssert.java 一部抜粋 compareDataメソッドの中で {{code Java, AssertThrowableList errors = new AssertThrowableList(); for (int i = 0; i < expectedTable.getRowCount(); i++) { // iterate over all columns of the current row for (int j = 0; j < comparisonCols.length; j++) { ComparisonColumn compareColumn = comparisonCols[j]; String columnName = compareColumn.getColumnName(); DataType dataType = compareColumn.getDataType(); Object expectedValue = expectedTable.getValue(i, columnName); Object actualValue = actualTable.getValue(i, columnName); // Compare the values if (skipCompare(columnName, expectedValue, actualValue)) { if (logger.isTraceEnabled()) { logger.trace( "ignoring comparison " + expectedValue + "=" + actualValue + " on column " + columnName); } continue; } if (dataType.compare(expectedValue, actualValue) != 0) { Difference diff = new Difference( expectedTable, actualTable, i, columnName, expectedValue, actualValue); try { // Handle the difference (throw error immediately or something else) failureHandler.handle(diff); } catch (Throwable th){ errors.add(th); } } } } if (!errors.isEmpty()){ throw new Error( errors.toString()); } }} !!Assertion.assertEquals 時に値をトリムする方法 各テーブルの項目が文字列の場合に、右側の空白をトリムして比較したい場合、 ITable を継承して独自の Tableオブジェクトを作ってしまう。 複雑な変換とか ReplacementDataSet ではやりきれない場合は、この方法を使うといいかもしれない。 !TrimTable.java {{code Java, package trim; import org.dbunit.dataset.ITable; import org.dbunit.dataset.ITableMetaData; import org.dbunit.dataset.Column; import org.dbunit.dataset.DataSetException; import org.dbunit.dataset.datatype.StringDataType; public class TrimTable implements ITable { protected ITable table; protected ITableMetaData metaData; public TrimTable(ITable table){ this.table = table; this.metaData = table.getTableMetaData(); } public int getRowCount() { return table.getRowCount(); } public ITableMetaData getTableMetaData() { return metaData; } public Object getValue(int row, String columnName) throws DataSetException { // Column[] columns = metaData.getColumns(); // Column column = columns[metaData.getColumnIndex(columnName)]; Object value = table.getValue(row, columnName); // if (column.getDataType() instanceof StringDataType && value != null) { if (value instanceof String) { value = rtrim((String)value, " "); // System.out.println("["+value+"]"); } return value; } private static String rtrim(String str, String del){ int start = 0; int end = str.length(); int length = del.length(); if (length == 0){ return str; } while(start <= end - length){ if (str.substring(end - length, end).equals(del)){ end -= length; } else { break; } } return str.substring(start, end); } } }} !呼出 Assertion.assertEquals(new TrimTable(expectedTable), actualTable); !!AmbiguousTableNameExceptionが発生する場合 環境によっては、 AmbiguousTableNameException が発生する場合がある。複数のスキーマで同名のオブジェクトがあると発生することがある。 その場合は、DatabaseConnectionのコンストラクタにスキーマを指定する。 DatabaseConnection con = new DatabaseConnection(db.getConnection(), "TEST_SCHEMA"); みたいな感じ。 !!!環境まるごと とりあえず、自分が実験した環境をまるごと置いておく。 *基本的なもの {{ref junit_allInOne_base.zip}} xlsxに対応したものを含めたもの {{ref junit_allInOne_xlsx.zip}} *xlsx対応やテーブルの差異すべて表示や項目のトリム処理を含めたもの {{ref junit_allInOne.zip}} {{category2 プログラミング言語,Java}}