Spring FrameworkのUnitテスト実装方法 2-1.Serviceテスト(Junit4, spring-test)

Spring FrameworkのUnitテスト実装方法 2-1.Serviceテスト(Junit4, spring-test)

Spring FrameworkのUnitテスト実装方法 2-1.Serviceテスト(Junit4, spring-test)

【サンプルソース】
TERASOLUNA Server Framework for Java (5.x) Development Guideline
サンプルソースはこちら
(これのMyBatis3を使用したパターンで作成してます。)

2-1.Serviceテスト Junit4,Spring-testライブラリを使用したパターン

Serviceクラスのテストです。
Spring-testライブラリを使うことによりDIコンテナを使うことができるので、
RepositoryクラスとServiceクラスを繋げた試験をすることができます。
今回使用しているサンプルの例だと、ServiceクラスでRepositoryクラスのUpdateメソッドを呼び出しているのですが、
Repositoryクラスをモック化しないでそのままテーブルのデータを更新する処理が走る感じです。

【pom.xml】

JunitライブラリとSpring-testライブラリが必要です。

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

【テスト対象クラス】

TodoService.java
テスト対象はfinishメソッド

public interface TodoService {
    Collection<Todo> findAll();
    
    Todo create(Todo todo);
    
    Todo finish(String todoId);
    
    void delete(String todoId);
}

今回のテストパターンの場合、DIコンテナを使うのでテストメソッドで試験する時に対象となるのはこのTodoServiceインターフェースになります。
インターフェースを実装したクラスもあるのですが、こちらを直接テストメソッドで指定してもDIコンテナの方を通らずにうまくRepositoryクラスにアクセスできずエラーとなります。
Serviceインターフェースを指定してDIコンテナから実装クラスを呼び出して試験するという、間接的に使用するイメージですかね。
ちなみに実装クラスは以下のファイルです。
テスト対象となるfinishメソッドの部分だけ切り出しました。

TodoServiceImpl.java

@Service
@Transactional
public class TodoServiceImpl implements TodoService {

    private static final long MAX_UNFINISHED_COUNT = 5;

    @Inject
    TodoRepository todoRepository;

    @Override
    public Todo finish(String todoId) {
        // TODO 自動生成されたメソッド・スタブ
        Todo todo = findOne(todoId);
        if (todo.isFinished()) {
            ResultMessages messages = ResultMessages.error();
            messages.add(ResultMessage.fromText("[E002]The requested Todo is already finished. (id=" + todoId + ")"));

            throw new BusinessException(messages);
        }
        todo.setFinished(true);
        todoRepository.update(todo);
        return todo;
    }
}

【テストクラス】

TodoServiceImplTest.java

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:META-INF/spring/test-context.xml"})
@Transactional
public class TodoServiceImplTest {

    @Inject
    TodoService target;
    
    @Before
    public void setUp() throws Exception {
        //@sqlアノテーションで指定したsqlファイルによってセットアップを実行するため、処理なし
    }
    
    //正常に動作したパターン
    @Test
    @Rollback
    //テストデータのセットアップに@sqlアノテーションを使っている
    @Sql("classpath:database/test_data_testFinishOK.sql")
    public void testFinishOK() throws Exception{
        //引数設定
        String todoId = "cceae402-c5b1-440f-bae2-7bee19dc17fb";
        
        //finishメソッドのテスト
        Todo todo = target.finish(todoId);
        
        //結果検証(assertTodoメソッドはメソッドの実行によって返ってきたTodoオブジェクトを検証するprivateメソッド)
        assertTodo(todo);
        
    }
    
    //取得したTodoオブジェクトのfinishedが既にtrueで異常が発生したパターン
    //@Testのexpectedには期待するExceptionクラスを設定する。こうすることでExceptionが発生することは想定通りということを宣言している。
    @Test(expected = BusinessException.class)
    @Rollback
    //テストデータのセットアップに@sqlアノテーションを使っている。わざとExceptionを発生させるため、testFinishOKメソッドの時のセットアップとデータを変えている。
    @Sql("classpath:database/test_data_testFinishNG.sql")
    public void testFinishNG() throws Exception{
        //引数設定
        String todoId = "cceae402-c5b1-440f-bae2-7bee19dc17fb";
        
        //try-catch文はなくてもJunitとしては正常になるが、printStackTraceメソッドでエラーの内容を表示させている。
        try {
            target.finish(todoId);
        }catch (BusinessException e) {
            // TODO: handle exception
            e.printStackTrace();
            
            throw e;
        }
    }
}

想定通りにデータを取ってきた時のパターンと、
異常が発生した時のパターンの2パターンを記述しました。
期待通りの異常が発生した場合は @Test(expected = Exceptionクラス)と設定することで、Junitのテストが成功になります。

【その他設定】

test_data_testFinishOK.sql

/* define the schemas. */
CREATE TABLE IF NOT EXISTS todo (todo_id VARCHAR(36) PRIMARY KEY, todo_title VARCHAR(30), finished BOOLEAN, created_at TIMESTAMP);
DELETE FROM todo;

/* load the records. */
INSERT INTO todo (todo_id, todo_title, finished, created_at) VALUES ('cceae402-c5b1-440f-bae2-7bee19dc17fb', 'one', false, '2017-10-01 15:39:17.888');
INSERT INTO todo (todo_id, todo_title, finished, created_at) VALUES ('5dd4ba78-ff5b-423b-aa2a-a07118aeaf90', 'two', false, '2017-10-01 15:39:19.981');
INSERT INTO todo (todo_id, todo_title, finished, created_at) VALUES ('e3bdb9af-3dde-40b7-b5fb-4b388567ab45', 'three', false, '2017-10-01 15:39:28.437');
commit;

test_data_testFinishNG.sql

/* define the schemas. */
CREATE TABLE IF NOT EXISTS todo (todo_id VARCHAR(36) PRIMARY KEY, todo_title VARCHAR(30), finished BOOLEAN, created_at TIMESTAMP);
DELETE FROM todo;

/* load the records. */
INSERT INTO todo (todo_id, todo_title, finished, created_at) VALUES ('cceae402-c5b1-440f-bae2-7bee19dc17fb', 'one', true, '2017-10-01 15:39:17.888');
INSERT INTO todo (todo_id, todo_title, finished, created_at) VALUES ('5dd4ba78-ff5b-423b-aa2a-a07118aeaf90', 'two', false, '2017-10-01 15:39:19.981');
INSERT INTO todo (todo_id, todo_title, finished, created_at) VALUES ('e3bdb9af-3dde-40b7-b5fb-4b388567ab45', 'three', false, '2017-10-01 15:39:28.437');
commit;

あらかじめfinishメソッド対象のデータをtrueとしておくことで異常を発生させる。

一点注意なのですが、BusinessExceptionというのはTERASOLUNAの機能になるので、
Springの単体テストについて見に来た方は「なんだこれ?」って思うかもしれませんが無視して大丈夫です。
あとResultMessagesオブジェクトとかもTERASOLUNAの機能です。

サンプルソースはgithubで公開してます。
Spring Unit Test pattern8

Junitカテゴリの最新記事