Selenium + REST-Assured を組み合わせた自動 API テスト

Web API を開発しているときに、API 単体のテストであれば様々な UnitTest ツールを使用してテストの自動化を行う事ができますが、Web サイトとのインタラクションが必要な場合にちょっと困ります。
一例としては、OAuth などの認証が前提のAPI処理を書く時などです。SPA などで構築されていてブラウザで表示しないと意図した動作にならないケースなどでは、どうしてもブラウザ経由でアクセスする必要があります。

今回、Webの自動テストに **Selenium** 、API のテストに **REST-Assured** を用いてテストの自動化を行ってみたので、簡単にまとめてみます。

# Tools
## Selenium
https://github.com/SeleniumHQ/selenium
言わずと知れたWeb Testツールです。
Java の API も存在するので、Java から直接操作することが出来ます。

## REST-Assured
https://github.com/rest-assured/rest-assured
Method Chain 的な書き方で REST API のテストを書くことができるライブラリです。

以下は、README に乗っているサンプル。

```java
given().
    param("key1", "value1").
    param("key2", "value2").
when().
    post("/somewhere").
then().
    body(containsString("OK"));
```

**given()** でパラメータを渡し、 **when()** で呼び出す API を指定し、 **then()** で期待する条件を記載する、という書き方になります。

# Sample
以下のようなシナリオで、Web ログインを前提とした API テストを書いてみます。

- ログインページに ID、Password を入力し、認証を行う。
- 認証の結果、 Session Cookie が払い出される。
- API に Session Cookie 付きのリクエストを行う。

## chromedriver
Selenium で Web Test を行うためには、ブラウザと、それに対応した driver が必要になります。

ここではブラウザに Chrome を使用するので、 driver も chromedriver をダウンロードします。以下のサイトから、各 OS 向けの driver をダウンロードすることが出来ます。

https://chromedriver.chromium.org/downloads

使用する chromedriver は、Chrome ブラウザのバージョンと一致している必要があるので、注意してください。
（Chrome のバージョンが 81.* の場合、 chromedriver も同じバージョンである必要がある）

## build.gradle
selenium と rest-assured を設定します。
今回使用したバージョンでは selenium の dependencies として設定されている guava が古かったようで、手で exclude して任意のバージョンの guava を使用するようにしました。

ref. https://stackoverflow.com/questions/50267144/chrome-options-selenium-3-10-nosuchmethoderror-com-google-common-collect-im

```
            // selenium
            dependencySet(group: 'org.seleniumhq.selenium', version: "3.141.59") {
                entry ('selenium-java') {
                    exclude group: "com.google.guava", name: "guava"
                }
                entry ('selenium-support') {
                    exclude group: "com.google.guava", name: "guava"
                }
                entry ('selenium-chrome-driver'){
                    exclude group: "com.google.guava", name: "guava"
                }
            }
            dependency "com.google.guava:guava:29.0-jre"

            // rest-assured
            dependency "com.jayway.restassured:rest-assured:2.9.0"
```

## Tset class
以下のような環境があるとして、ログイン〜 API 呼び出しまでの自動テストを書いてみます。ドメインなどは仮のもので、実際には存在しません。

- ログインページ: https://wwww.moaikids.com/login
  - 以下の情報をログイン情報として入力する。
    - username
    - password 
- Cookie: session という名前の Cookie にセッション情報が書かれる。 
- API: https://api.moaikids.com/api/check
  - 正常時は以下のような JSON が返却される。

```
{
  "status": "ok"
}
``` 

### SampleTest

```
package webtest;

import com.jayway.restassured.builder.RequestSpecBuilder;
import com.jayway.restassured.response.Response;
import com.jayway.restassured.specification.RequestSpecification;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;

import static com.jayway.restassured.RestAssured.given;
import static com.jayway.restassured.path.json.JsonPath.from;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

class SampleTest {
    private RequestSpecification spec;
    private String session;

    @BeforeEach
    void setup() {
        System.setProperty("webdriver.chrome.driver", "./src/test/resources/chrome/chromedriver");
        ChromeOptions options = new ChromeOptions().addArguments("--headless");
        WebDriver driver = new ChromeDriver(options);

        driver.get("https://wwww.moaikids.com/login");
        driver.findElement(By.name("username")).sendKeys("username");
        driver.findElement(By.name("password")).sendKeys("password");
        WebElement element = driver.findElement(By.className("login"));
        element.submit();
        Cookie cookie = driver.manage().getCookieNamed("session");

        session = cookie.getValue();
        driver.close();

        RequestSpecBuilder build = new RequestSpecBuilder();
        build.setBaseUri("https://api.moaikids.com")
                .addCookie("session", session);
        spec = build.build();
    }

    @Test
    void ok__check() {
        Response response = given().spec(spec).get("/api/check");
        response.then().statusCode(200);
        String json = response.asString();
        System.out.println(json);

        assertNotNull(from(json).get("status"));
        assertEquals(from(json).get("status"), "ok");
    }
}
```

#### driverの指定
System Property に chromedriver を指定します。

```
System.setProperty("webdriver.chrome.driver", "./src/test/resources/chrome/chromedriver");
```

#### selenium の option 指定
テストで使用するために、 Headless モードを指定します。

```
ChromeOptions options = new ChromeOptions().addArguments("--headless");
WebDriver driver = new ChromeDriver(options);
```

#### ログインフォームへの情報の入力
input form に情報を入力する際は、 **sendKeys** メソッドを使用します。

```
driver.get("https://wwww.moaikids.com/login");
driver.findElement(By.name("username")).sendKeys("username");
driver.findElement(By.name("password")).sendKeys("password");
driver.findElement(By.className("login")).submit();
```

#### Cookie 情報の取得
session という Cookie から session 情報を取得します。

```
Cookie cookie = driver.manage().getCookieNamed("session");
session = cookie.getValue();
```

#### REST-Assured の RequestSpecification を作成
REST-Assured によるテストは色々な書き方がありますが、複数 API に対して共通の処理を行う場合は RequestSpecification を作成すると便利です。

以下は、 **https:/api.moaikids.com** 配下の API に対するリクエストの際には session という名前の Cookie を必ず送信する、という例です。

```
RequestSpecBuilder build = new RequestSpecBuilder();
build.setBaseUri("https://api.moaikids.com")
            .addCookie("session", session);
spec = build.build();
```

#### API Test
**/api/check** に対してリクエストを行い、以下について評価をします。

- Response で Status Code 200 が返却される。
- Response Body の JSON に "status" という要素がある
- "status" の値が "ｏｋ" である。

```
    @Test
    void ok__check() {
        Response response = given().spec(spec).get("/api/check");
        response.then().statusCode(200);
        String json = response.asString();
        System.out.println(json);

        assertNotNull(from(json).get("status"));
        assertEquals(from(json).get("status"), "ok");
    }
```

## Running
上記のテストケースを実行すると、Selenium が起動し、ブラウザを用いて処理が実行されます。

```
Starting ChromeDriver 81.0.4044.69 (6813546031a4bc83f717a2ef7cd4ac6ec1199132-refs/branch-heads/4044@{#776}) on port 19278
Only local connections are allowed.
Please protect ports used by ChromeDriver and related test frameworks to prevent access by malicious code.
[1588734871.003][WARNING]: FromSockAddr failed on netmask
5月 06, 2020 12:14:31 午後 org.openqa.selenium.remote.ProtocolHandshake createSession
情報: Detected dialect: W3C
[1588734871.624][SEVERE]: Timed out receiving message from renderer: 0.100
[1588734871.726][SEVERE]: Timed out receiving message from renderer: 0.100

(snip.)
```

then() や assert** などで指定した条件をクリアしたらテストは OK 、意図した結果と異なる場合エラーとなります。以下はステータスコード 200 を期待していたが 403 を返却されたケースです。

```
java.lang.AssertionError: 1 expectation failed.
Expected status code <200> doesn't match actual status code <403>.
```

# まとめにかえて
Selenium と REST-Assured を組み合わせると、 Web Login などブラウザの使用を前提とした処理でも比較的簡単に自動 Test を記述することが出来ました。

実行にはブラウザが必要なので、 CI などで実行する際は実行環境にブラウザを入れるなどの工夫が必要になります。