SpringBootで作ったRestAPI/Webアプリのテストを書いてみた


前回のエントリ「[SpringBoot+Doma2+Gradleを試してみた。](http://qiita.com/nyasba/items/1e22c2401f3849f9071d)」のテストを書いてみたので公開してみる。

RestAPI　→　RestAssured
Webアプリ　→　Geb

もちろん題材は「[はじめてのSpring Boot](http://www.amazon.co.jp/dp/4777518655)」にある顧客管理システムです。

ソースコードはこちら。
https://github.com/nyasba/domaboot


## RESTAPI部分

ここは本に出てくる内容とほぼ同じです。

### 準備

rest-assuredをgradleの依存関係に追加するだけです。

```build.gradle
testCompile("com.jayway.restassured:rest-assured:2.3.3")
```

### テストコード

#### 一覧取得

RestAPIはList<Entity>で返却されるので、パラメータに[0],[1]などのindexが必要です。

```CustomerRestControllerIntegrationTest.java
    @Test
    public void 一覧取得を行う() {
        when()
                .get("/api/customers")
        .then()
                .statusCode(HttpStatus.OK.value())
                .body("id[0]", is(customer1.getId()))
                .body("lastName[0]", is(customer1.getLastName()))
                .body("firstName[0]", is(customer1.getFirstName()))
                .body("id[1]", is(customer2.getId()))
                .body("lastName[1]", is(customer2.getLastName()))
                .body("firstName[1]", is(customer2.getFirstName()));
    }
```

#### ID指定取得を行う

こちらはListではないのでパラメータ名そのままでOK

```CustomerRestControllerIntegrationTest.java
    @Test
    public void ID指定取得を行う() {
        when()
                .get("/api/customers/{id}", 101)
        .then()
                .statusCode(HttpStatus.OK.value())
                .body("id", is(customer1.getId()))
                .body("lastName", is(customer1.getLastName()))
                .body("firstName", is(customer1.getFirstName()));
    }
```

#### 新規登録を行う

ContentTypeは指定するメソッドがあるようです。
文字コード指定はconfigでEncorderConfigがあるのでそれで指定したらできました。

body要素はEntityオブジェクトをそのまま渡す形でいけるようですが、今回はこれでテストしました。

```CustomerRestControllerIntegrationTest.java
    @Test
    public void 新規登録を行う() {
        CustomerEntity SIZUKA_CHAN = new CustomerEntity(1, "宮本", "しずか");

        given()
                .contentType(ContentType.JSON)
                .config(getUTF8Config())
                .body(String.format("{ \"lastName\":\"%s\", \"firstName\":\"%s\" }", SIZUKA_CHAN.getLastName(), SIZUKA_CHAN.getFirstName()))
        .when()
                .post("/api/customers")
        .then()
                .statusCode(HttpStatus.CREATED.value());

        assertThat(customerRepository.findById(1), is(SIZUKA_CHAN));
    }

    private RestAssuredConfig getUTF8Config() {
        return new RestAssuredConfig().encoderConfig(
			 EncoderConfig.encoderConfig().defaultContentCharset("UTF-8")
		 );
    }
```

#### 更新

```CustomerRestControllerIntegrationTest.java
    @Test
    public void 更新を行う() {
        CustomerEntity DORAMI = new CustomerEntity(101, "どら", "ミ");

        given()
                .contentType(ContentType.JSON)
                .config(getUTF8Config())
                .body(String.format("{ \"lastName\":\"%s\", \"firstName\":\"%s\" }", DORAMI.getLastName(), DORAMI.getFirstName()))
        .when()
                .put("/api/customers/{id}", 101)
        .then()
                .statusCode(HttpStatus.OK.value());

        assertThat(customerRepository.findById(101), is(DORAMI));
    }
```

#### 削除

```CustomerRestControllerIntegrationTest.java
    @Test
    public void 削除を行う() {
        when()
                .delete("/api/customers/{id}", 101)
        .then()
                .statusCode(HttpStatus.NO_CONTENT.value());

        assertThat(customerRepository.findById(101), is(nullValue()));
    }
```


## Webアプリ部分

Geb（じぇぶ）を使います。GebはWebアプリのブラウザテストを実行するためのフレームワークで、groovyを使って記述します。seleniumを使ったテストを簡単に書けるもの、といった方がわかりやすいかもしれません。

### 準備

groovyを使うので、javaからgroovyに変えます

```build.gradle
apply plugin: 'groovy'
```

依存関係には以下を追加。

```build.gradle
    /* SpockFramework(GroovyのBDDテストフレームワーク) */
    testCompile("org.spockframework:spock-core:0.7-groovy-2.0")

    /* Geb(ブラウザテスト用フレームワーク) */
    testCompile "org.gebish:geb-spock:0.10.0"

    /* Webドライバ */
    testCompile "org.seleniumhq.selenium:selenium-chrome-driver:2.44.0"
```

Gebの設定用オブジェクト。
Chromeで実行するための設定です。chromedriverは[ここ](https://code.google.com/p/selenium/wiki/ChromeDriver)から各環境に適したものをダウンロードしておく必要があります。

```src/test/resources/GebConfig.groovy
import org.openqa.selenium.chrome.ChromeDriver

System.setProperty("webdriver.chrome.driver", "driver/chromedriver.exe")

driver = {
    return new ChromeDriver()
}
```

### テストコード

PageObjectパターンを使って実装しました。

#### メインページのPageObject

```MainPage.groovy
package com.example.web.page

import geb.Page

/**
 * メインページ
 */
class MainPage extends Page {

    // to MainPage でアクセスするURLを定義
    static url = "http://localhost:8888/customers"

    // at MainPage で満たすべきassert条件を記載
    static at = { title == "顧客管理システム" }

    // MainPageのコンテンツ
    static content = {

        姓は { $('#lastName').value(it) }
        名は { $('#firstName').value(it) }
        で登録する { $('#register').click() }

        登録件数 {
            $('#customer-list').find("tbody").children().size()
        }

        名前 {
            $("#lastName${it}").text() + $("#firstName${it}").text()
        }

        編集ボタンを押す(to: EditPage){
            $("#edit${it}").click()
        }

        削除ボタンを押す(to: MainPage){
            $("#delete${it}").click()
        }
    }
}
```

#### 編集ページのPageObject

```EditPage.groovy
package com.example.web.page

import geb.Page

/**
 * 編集ページ
 */
class EditPage extends Page {

    // at MainPage で満たすべきassert条件を記載
    static at = { title == "顧客情報編集" }

    // MainPageのコンテンツ
    static content = {

        姓は { $('#lastName').value(it) }
        名は { $('#firstName').value(it) }
        で更新する( to : MainPage) { $('#submit').click() }

        やっぱり戻る { $('#goToTop').click() }
    }
}
```

#### テストシナリオ

実行結果から紹介します。
この順に実行されますので、メソッド名と比較して確認していただけるとわかりやすいかと思います。

　![スクリーンショット 2015-01-04 00.29.03.png](https://qiita-image-store.s3.amazonaws.com/0/47589/d0ccb94b-3d1e-1051-42aa-d51d58bc08fb.png "スクリーンショット 2015-01-04 00.29.03.png")


setupSpec()はテストの一番最初にだけ実行されます。その中でWebアプリを立ち上げています。（これがテストとして適した方法かはわかってません）


あと、テストコード中の「で登録する」や「で更新する」というのはPageクラスで定義したメソッドですので。


```CustomerGeb.groovy
package com.example.web.story

import com.example.App
import com.example.web.page.EditPage
import com.example.web.page.MainPage
import geb.spock.GebSpec
import spock.lang.Stepwise
import spock.lang.Unroll

@Unroll      // メソッド名に変数を差し込めるようにする
@Stepwise    // 上から順に実行するようにする
class CustomerGeb extends GebSpec {

    def setupSpec() {
        App.main([
                "--server.port=8888",
                "--spring.profiles.active=test"
        ] as String[])
    }

    // whereブロックの変数名を引数に設定。それらをINPUTにメソッドが繰り返し呼ばれる
    def "「#lastName#firstName」を登録する"(int num, String lastName, String firstName) {

        setup:
            to MainPage
            at MainPage
            assert 登録件数 == num -1

        expect:
            姓は lastName
            名は firstName
            で登録する

            assert 登録件数 == num
            assert 名前(num) == lastName + firstName

        // テストに必要なINPUTを定義するブロック
        where:
            num | lastName | firstName
            1   | "剛田"     | "たけし"
            2   | "どら"     | "えもん"
    }

    def "_#num番目を「#lastName#firstName」に変更する"(int num, String lastName, String firstName){

        setup:
            to MainPage
            at MainPage

        expect:
            編集ボタンを押す(num)

            at EditPage

            姓は lastName
            名は firstName
            で更新する

            at MainPage
            assert 名前(num) == lastName + firstName

        where:
            num | lastName | firstName
            1   | "きれいな"     | "ジャイアン"
    }

    def "_#num番目を「#lastName#firstName」に変更しようとしてやっぱり戻る"(int num, String lastName, String firstName){

        setup:
            to MainPage
            at MainPage

        expect:
            def 編集前の名前 = 名前(num)
            編集ボタンを押す(num)

            at EditPage

            姓は lastName
            名は firstName
            やっぱり戻る

            at MainPage
            assert 名前(num) == 編集前の名前

        where:
            num | lastName | firstName
            2   | "どら"     | "みちゃん"
    }

    def "_1番目を削除する"(){

        setup:
            to MainPage
            at MainPage

        when:
            assert 登録件数 == 2
            def 編集前の名前 = 名前(2)
            削除ボタンを押す(1)

        then:
            at MainPage
            assert 登録件数 == 1
            assert 名前(1) == 編集前の名前
    }

}
```

おわり。