SAStruts/S2JUnit4でserviceクラスをテストする

目次

SAStruts/S2JUnit4でserviceクラスをテストする

ここでの記述は特に Service(データベース関係の処理)だけでなくもちろん、Entity や Form にも応用可能である。

まず使ってみる

プラグインの準備

Eclipseのヘルプ→新規ソフトウェアインスコから

http://eclipse.seasar.org/updates/3.3/

を追加して、S2Junit プラグインを入れる。

テストクラスの準備

テスト対象の service が HogeService だったとして、そいつを・・・

  1. 右クリック
  2. コンテキストメニュー
  3. S2Junit4テスティングペアを開く

操作

無いから作るか?と聞かれるので、作る。

そうすると

src/test/java/hoge/piyo/fuga/service

以下に HogeServiceTest.java が自動的に作成されて、中身は・・・

package hoge.piyo.fuga.service;
 
import static org.seasar.framework.unit.S2Assert.*;
import org.junit.runner.RunWith;
import org.seasar.framework.unit.Seasar2;
 
@RunWith(Seasar2.class)
public class HogeServiceTest {
    private HogeService hogeService;
    public void testPiyo() {
        System.out.println("hello");
        fail("まだ実装されていません");
    }
}

・・・このようになる。これは自動的に作ってもらったが別に手で作っても構わない

テスト用の設定をする

テスト用の設定は

src/test/resources

以下に収める。

通常だったらs2junit4.diconのみがあるはず。

基本的に通常の

src/main/resources

の設定がそのまま使われる。

テスト独自の設定をしたいならば

src/test/resources

に設定を変えたいdiconファイルを

src/main/resources

からコピーして変えたい部分だけを書き換えるとそっちが優先的に読み込まれるっぽい

テストの実行

  1. HogeServiceTest.javaを右クリック
  2. 実行
  3. JUnit

でテストが実行される。

結果がJUnitビューに失敗(赤バー)として表示されているだろう。OK!

特定のテストメソッドの前処理をする

特定のテストメソッド、testHogeメソッドの前処理をしたい場合は

beforeTestHoge

メソッドを実装する

特定のテストメソッドの後処理をする

特定のテストメソッド、testHoge メソッドの後処理をしたい場合は

afterTestHoge

メソッドを実装する

テストデータの準備

S2Junit4ではテストデータをエクセルで書いて取り込めるという便利なんだか不便なんだかよくわからん機能があるのでこいつを使ってデータを突っ込んでみる。※個人的にはエクセルファイルのような不透明で複雑な情報を持つファイルでテストデータを作るというのはどうかと思う。

テストファイル名は・・・

テストクラス名_テストメソッド名.xls

・・・となる。この設定はs2junit4.diconに書いてあるので変えることもできる。 このフォーマットではテストを大量に作っていくうちにパッケージ内がグチャグチャになるので、スラッシュ区切りにしてディレクトリにわけるとよい。

そしてこのファイルをHogeServiceTest.javaと同じパッケージに保存

  • sheet名に対象のテーブル
  • 1行目にカラム名
  • 2行目移行にデータ

のように記述する

そうするとテスト実行時にそのメソッドになったらエクセルに記述したデータをDBに突っ込んでくれる。 テスト終了後に自動的にロールバックされるので突っ込んだデータの後始末は考えなくてよい

実際の設定は↓のようになっている。ここで注目は2個書いてあるということ

<component class="org.seasar.framework.unit.impl.TestDataPreparerImpl">
    <initMethod name="addTestDataXlsPath">
        <arg>context.testClassShortName + "_" + context.testMethodName + ".xls"</arg>
    </initMethod>
    <initMethod name="addTestDataXlsPath">
        <arg>context.testClassShortName + ".xls"</arg>
    </initMethod>
</component>

ここでテストクラス名.xlsとすれば、テストクラス全体で読み込んでくれるということ。 設定としては先勝ちっぽいのでゆるい設定ほど後に書いていけばいいと思う

ぜんぜん違う名前のxlsファイルとかぜんぜん違うテストケース間で同じテストデータが使いたい場合は自動的なローディングはやめてDataAccessorを使ってテストメソッドごとに個別に設定できるみたい。

public class PiyoTest{
    private DataAccessor accessor;    
    public void testFuga1(){
        accessor.readXlsAllReplaceDb("HogeTest.xls");
    }
}

DataAccessor自体は書いておけばSeasar側が勝手に突っ込んでくれる。コレで任意のテストデータのエクセルファイルを任意のテストケースで使えるようになる。

モックを使ってテストする

テストクラスに

@EasyMock
private HogePiyo hogePiyo;
public void recordGetFuga throws Exception{
    expect(hogePiyo.getFuga()).andReturn("ふがのもっく");
}

のように書くらしい

そうするとテスト対象クラスのメソッドがHogePiyoに依存してgetFugaを呼び出している部分がexpectで定義した内容に変わるらしい・・

DIされるインスタンスを制御する

対象のテストメソッドが内部でインスタンスをインジェクションされていてその内部状態を利用している場合。 テストクラスにも同様のプロパティを作ってインジェクションしてもらい、その内部状態をテストメソッドで書き換える。

こうすることで、テスト対象クラス内部のインスタンスの値を制御できる。

ま、そもそもそうういう設計にしないというのが筋。

テスト後にロールバックさせたくない

開発の初期の段階だと単に実行して自分でDBを見て値検証したい時ってある。テストメソッドを単なるインジェクション+実行トリガとして使いたい場合。こういう時は自動ロールバックがウザイ場合があるので切りたい。

テストメソッドにアノテーションで

@TxBehavior(TxBehaviorType.NONE)

と書けば抑止できる

特定のテストメソッドを無視する

一時的にテストを止めておきたい場合に別にコメントアウトしてもいいわけだがアノテーションで1行書くと無視してくれる

@Ignore
pubilc void testHoge(){
}

例外の発生をテストする

このように書く

@Test(expected=HogeException.class)
pubilc void testHoge(){
    piyo()//例外が発生する処理
    fail("例外が発生しなかったのでエラー"); //発生するとこの行には来ない
}

テスト用のDBにもう既に値が入っているのだが消したく無い

TestContext ctx;
 
public void before(){
    ctx.setPreparationType(PreparationType.ALL_REPLACE);
}

テストクラスに↑のように書きこんでおけば、エクセルファイル等からのデータのツッコミ前にテーブルデータを全部消してくれる。 テスト後はロールバックするので元に戻る。

単にテーブルの既存データが邪魔ならば

private DataAccessor accessor;
public void testHoge(){
    accessor.deleteTable("hoge");
}

で消すことができる

長いテーブル名のテーブルにテストデータを突っ込む

S2Junit4の規約ではデータベースのテーブル名とエクセルのシート名が対応している。・・・のだが

MySQLの識別子の限界が64文字らしくエクセルシートの文字の限界が31文字なのだ。つまり対応できない状況があるのだ。

まず長い名前のテーブルのデータを適当な短い名前でシートを作る。

private DataAccessor accessor;	
public void testHoge(){
    DataSet ds = accessor.readXls("HogeTest_testHoge_data.xls");
    DataTable dt = ds.removeTable("hoge_piyo");
    dt.setTableName("hoge_piyo_fuga_hoge_piyo_fuga_hoge_piyo_fuga");
    ds.addTable(dt);
    accessor.writeDb(ds);		
}

という感じに

  • エクセルからDataSetで読みだす
  • DataSetから短い名前でシートに対応するDataTableを削除しつつ取り出す(removeTableの戻りは削除対象のDataTable)
  • 取り出したDataTableの名前を本来の長いものに書き換える
  • DataSetに再び戻す
  • DataSetをDBへ書き込む

トラブルシューティング

ドキュメント、他の事例も充実してないのでとにかくなんか変なことでトラブルことが多い。メーリングリストで質問しろといえばそれまでなんだがね・・・

エクセルからの日付カラムがうまく入らなくて怒られる

↓のように認識エラーが出る。そのままMySQLにつっこんでくれたら入りそうだけど、一旦Java側で咀嚼するようでエラーがでる

java.text.ParseException: Unparseable date: "2010-06-24 15:46:38"

これは、日付カラムはエクセルファイル上でも日付型でないといけない。なので書式から日付型に変更したらうまくいくかも

エクセルから入れた初期データがロールバックしない

たぶんテーブルのエンジンがMyISAMになっている。MyISAMはトランザクションを張れないので当然ロールバックもできない。

InnoDBみたいなトランザクション管理できるエンジンに変更したらうまくいくかも

NULL値が入れられない

エクセルのセルの書式が関係しているみたい。セルごと除去して、値を作りなおすとうまくいくかも。

エクセルファイルのテストデータを変更したのにそれが反映されない

Eclipse上でリフレッシュしないと反映されない場合アリ。 というかテストデータ自体は編集しているファイルではなく、(Eclipseが自動的に)targetにコピーされたものを見に行っているのでこのシンクロがずれると反映されない。

エクセルファイルのテストデータ自体がテストに反映されない

テスト対象のメソッドが新規でトランザクションを開始する指定がされている場合になるかも

詳しくは→SAStruts/トランザクション管理

テスト対象のメソッドを実行した結果をassertで検証しているのだがどうしても値が合わない

テスト対象のメソッドが新規でトランザクションを開始する指定がされている場合になるかも

詳しくは→SAStruts/トランザクション管理

とにかくテストデータがうまく入らない

本物のエクセルでxlsファイルを作るとうまくいくかも。

LibreOffice3.4でxlsファイルを作ってやった場合失敗した。LibreOfficeで作ったファイルを本物のエクセルで開いた場合、表示や書式が若干おかしくなってしまう。日付カラムがユーザー定義になってしまったりする。

やっぱり正式じゃないとダメなのか?

通常実行では出ないIllegalAutoBindingPropertyRuntimeExceptionが出る

テストを実行すると通常実行では出ないIllegalAutoBindingPropertyRuntimeException

app.dicon

<component name="piyoFuga"  class="hoge.piyo.fuga.PiyoFuga" instance="singleton"/>

と書かかれている対象で問題がおきているみたいpiyoFuga自体はテストとまったく関係無いクラスなのだがsingletonだから初期化の時に1個作られてしまうっぽい

今のところテストの設定では記述しないようにしている。

Jettyを使って通信部分のテストを行う

外部とHTTPで通信をするテストもあると思う。めんどう。

ここでJettyというJava製のhttpサーバを使うと、起動、パラメータ、レスポンス設定、停止という処理をを全部Java上からコントロールできるようになる つまりテストコードでサーバ状態を再現できる!繰り返しテストができる!というもの

さっそくやってみる

インストール

Maven で一発でいれてください。

<dependency>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-server</artifactId>
    <version>9.2.1.v20140609</version>
    <scope>test</scope>
</dependency>

Jetty のテスト用レスポンス作成

テストクラスの内部クラスとして Jetty 制御用のクラスを作成する。

public class DummyOutput extends AbstractHandler{
    @Override
    public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException{
        System.out.println("target = " + target);
        response.setContentType("text/html");
        response.setStatus(HttpServletResponse.SC_OK);
        baseRequest.setHandled(true);
        InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("hogehoge_test" + target);
        OutputStream out = response.getOutputStream();
        byte[] buff = new byte[1024];
        int len = 0;
        while((len = in.read(buff, 0, buff.length)) != -1){
            out.write(buff, 0, len);
        }
    }
}

target には、ドメインのルートからの値が入ってくる。http://localhost:1234/hogehoge ならば /hogehogeが入る。 なのでこれによりテスト時のURL違いによる挙動の変化を場合分けすればよい。

レスポンスを直接String で書くこともできるが、このへんはファイルで記述したほうが楽だろう

テスト前に Jetty を起動する

before の prefix をつけてテスト前にJettyを起動するメソッドを作成する。

private Server server;
public void beforeTestHogehoge() throws Exception{
     server = new Server(1234);
     server.setHandler(new DummyOutput());
     server.start();
}

この記述で http://localhost:1234 で 起動した Jetty のサーバにアクセスできようになる。

Jetty のサンプルコードでよく start の後に join メソッドを呼び出しているものがある。 テストではデーモンのようにずっと待ち受けるわけではなく、テストが終わったら終了させたいので join は呼び出さない。

join を呼び出すとテストの実行スレッドが Jetty の終了を待ち続けるのでテストが進まなくなる。

終了時にも Jetty を呼び出したいのでメンバに格納する。

テストする

Jetty は期待するアウトプットのダミーを吐いてくれるので、それに対する挙動をテストすればよい。

テスト後に Jetty を終了する

after の prefix をつけてテスト後にJettyを終了するメソッドを作成する。

public void afterTestHogehoge() throws Exception{
    server.stop();
}

stop メソッドを呼び出すと終了する

org.hamcrest.Matchersのメソッド色々

文字列系

単純に一致

assertThat(result, is("ほげ"));

前方一致

assertThat(result, startsWith("ほ"));

後方一致

assertThat(result, endsWith("げ"));

指定した文字列で始まって、指定した文字列で終わる

assertThat(result, allOf(startsWith("ほ"), endsWith("げ")));

テストのソースコードのある位置のディレクトリのフルパスを取得する

テストコード内でこのように取得できる。

String path = this.getClass().getResource("").getPath();
// 状況にもよるがこのような値が取れる
// "/home/hogehoge/eclipse-workspace/foo/target/test-classes/com/example/teeesuuutooo/"

Java はソースコードがそのまま動くのではなく、実行時にコンパイルされて動くという性質上、ソースコードと実行ディレクトリが別になっている。 コンパイル時にJavaソースはコンパイルされ、それ以外のファイルは対象のディレクトリに構造まるごとコピーされる。

Java の実行時のファイルパスはそのことを意識する必要がある。

参考サイト

タグ

java/sastruts/service_test_by_s2junit4.txt · 最終更新: 2020-10-01 12:48 by ore