Spring Boot Spockでテストを行う

記事の内容

この記事ではSpring Bootで作った様々なAPI、メソッドに対してSpockを用いてテストを行う方法について紹介します。Spockの基本的な書き方、構造の説明もしています。

Spockの導入方法については以下を参考にしてください。

この記事では以下の記事で作成したMySQLを用いたプロジェクトを対象にテストを導入していきます。

Spockでのテストの書き方

まず、Spockでテストを書く前にSpockのテストの書き方について説明します。

テストコードとしてHealthTest.groovyがあったときその中にあるメソッドは以下の構造で書きます。

①の部分ではdefの隣の””の中にこのテストメソッドがどのようなことを行うか端的にわかるように書きます。

テストメソッドの中は②〜④のgiven, when, thenの3つで区切られており、given:以下にはDIするクラスのモック化や値を宣言する処理を書き、when:以下にはテストしたい処理を書きます。then:以下にはwhenの処理が行われたときにどのような結果、値になって欲しいかを評価するコードを書きます。

また、give:, when:, then: の隣の””内には説明を書くことができテストの見通しをよくしたり、理解し易くすることができます。

より具体的な例を書くと例えば以下のようなHealth.javaがあったときに、このクラスのテストを行うHealth.groovyは以下のように書くことができます。

Health.java

HealthTest.groovy

テストが複雑でなければgive:, when:, then:の横の説明は省略してもよいと思います。

実際に様々なテストを書いてみる

メソッドやAPIの動作確認テスト

それでは実際に様々なAPI、メソッドに対してテストを書いていきます。

例えば、BookControllerのBookの1件取得APIのテストの行い方を詳しく説明します。

このAPIの単純なメソッドに対するテストは以下のように行う。

BookControllerTest.groovy

上記テストの書き方について説明すると、

def target = new BookController()でテスト対象(target)のクラスを用意。

target.bookService = Mock(BookService)でBookControllerがDI(Autowired)しているBookServiceをモック化。

1 * target.bookService.getBook(1) >> new Book(1, “my title”, “my description”, false)でbookServiceのgetBookメソッドが()内の引数で呼ばれたら>>以下のものを返す。1 * 部分はbookService.getBookが1回だけ呼ばれたことをチェックする為のものである。

つまり、この部分はtarget内のbookService.getBookが引数に1を渡されて1回実行したときは、new Book(1, “my title”, “my description”, false)を返すということである。

補足として、この部分の書き方としてはこのモック化したメソッドが1回実行することを確認する必要がなければ_ * と書いても良い。また、引数に何が渡されても良い場合は.getBook(_)と書くこともできる。

次に、このAPIに対してAPIの動作を確認するテストを行う。

テストメソッドは以下のように書く。

単純なメソッドテストの違いとしてAPIのテストではdef mockMvc = MockMvcBuilders.standaloneSetup(target).build()の部分でtargetとなるAPIをモックとして起動している。

そして、def response = mockMvc.perform(get(“/book/1”))の部分でAPIに対してリクエストを投げている。

response.andExpect(status().isOk())ではAPIから返ってきたレスポンスのステータスコードが200 OKであることを確認し、assert response.andReturn().getResponse().getContentAsString() == ‘{“bookId”:1,”title”:”my title”,”description”:”my description”,”deleteFlag”:false}’ではレスポンスの中から取り出したレスポンスボディを文字列化してみて想定通りの値になっているかをチェックしている。(レスポンスボディをStringしてチェックする必要はなく値を一つ一つ取り出して確認しても良いです。また、ステータスコード以外にもレスポンスがJSONかなどチェックすることもできます)

イテレーションでテストを行う

上記のテストにおいてリクエストする内容やレスポンスする内容を変えていくつかのテストをしたい場合、Spockにあるイテレーションを使うことで簡単に行うことができる。

イテレーションを使うには以下のようにコードを変更する。

メソッドに@Unrollをつけることでそのメソッド内のwhere:で指定された値を一行ずつメソッド内の値に代入しテストをイテレーションで実行してくれる。

例えば上記メソッドを実行すると、1つ目のテストとしてbookId=1,title=”my title 1″,description=”my description 1″というように値が代入されてテストが実行され、次にbookId=2,title=”my title 2″,description=”my description 2″というように値が代入されテストが実行される。where以下に何行書かれてもその行分だけ同じように実行される。

(@UnrollをつけたときNo tests found for given includes…というエラーがでたら[Preferences] > [Build, Execution, Deployment] > [Build Tools] > [Gradle] > Run Tests usingをIntelliJ IDEAにする必要がある)

メソッド名のdefの隣の””の中に#bookId, #titleと書くことでテスト実行時に値が代入されて表示されるのでどのテストで成功、失敗したかがすぐにわかるようになる。

様々なパターンのテストを書いてみる

まだテストを書いていないクラスのメソッドをいくつか書いていく。

BookControllerのBookの作成APIのテストの行い方を書くと、BookControllerが以下のようにあるので、

テストメソッドは以下のように書く。

def request = new StringBuffer()の部分でrequestするパラメータをjson構造で作成している。

POSTメソッドのAPIにアクセスするのでmockMvc.perform(post(“/book/”)でpostであることを宣言し、.contentType(MediaType.APPLICATION_JSON)でリクエスト内容がjson構造であることを定義している。

response.andExpect(status().isCreated())は今回POSTメソッドで成功したときのHTTPステータスは201 created なのでisCreated()にしている。

インラインDB h2を用いてデータ操作のテストをする

次にBookRepositoryのテストを行ってみる。

Repositoryのテストは実際にDBからデータ取得したり更新したりしてみるので、インラインDBのh2を使う。

h2の設定にはbuild.gradleに以下のh2のライブラリを入れる。

src/test以下にresourcesフォルダを作りその下にapplication-test.ymlを追加する。

application-test.yml

インラインDBのh2を用いたテストで使うテーブルの作成定義をschema.sqlに、使うデータをdata.sqlに定義する。どちらもsrc/test/resources以下である。

schema.sql

data.sql

BookRepositoryの一件のデータ取得とレコードの作成メソッドは以下のようになっている。

BookRepository.java

このメソッドに対してテストを行うには以下のように書く。

@SpringBootTest, @ActiveProfiles(“test”)と書くことでこのテストの起動時にapplication-test.ymlを読み込み、schema.sqlのテーブルを作り、data.sqlのデータを挿入してくれるようになる。

ActiveProfilesの”test”と書いている箇所はapplication-test.ymlのハイフン以下の”test”の部分で紐づいてファイルを読み込んでいる。

他のクラスと違ってBookRepositoryはinterfaceなのでこれ自体をテストするにはAutowiredしなければならない。

def response = bookRepository.findByBookId(1)とすることでbookId=1のデータを取得できる。なぜ取得できるかはdata.sqlがすでにh2のDBで実行されてデータができているからである。

def response = bookRepository.save(book)でresponseのbookId=3なのはdata.sqlでbookId=1,2のデータは作っているからである。

@SpringBootTestはモック化ではなく実際にプロジェクトを起動しているので、今回はBookRepositoryのみで使ったが、BookControllerでも使うことで、API全体を通したテストやAPIのパラメータのバリデーションテストなども行うことができる。

参考

データ駆動テスト — Spock 1.0-SNAPSHOT
Spock Framework Reference Documentation