【Selenium】SpringBootで作成したアプリケーションの結合テスト

JUnitを使用した単体テスト書く経験は何回かありましたが、先日Seleniumを使用した結合テストを行ったので、要点をまとめたいと思います。

## Seleniumとは

Seleniumは、Webアプリケーションをテストするためのフレームワーク。
Java以外にもC#、Groovy、Perl、PHP、Python、Ruby、Scalaなどにも対応している。
pom.xmlのdependencyに追記することで、Javaからブラウザを使った結合テストを行うことができる。

## 環境

- macOS Sonoma 14.6.1
- Java 17
- Spring-Boot 3.3.3
- selenium-java 4.24.0
- webdrivermanager 5.9.2
- Thymeleaf
- Maven
- ChromeDriver 

## 実際にやってみた

簡単なWebアプリケーションを作成して試してみました。

#### Controller/Repository

importは省略します

```java:SeleniumRepository

@Repository
@Transactional
public class SeleniumRepository {
    @Autowired
    private NamedParameterJdbcTemplate template;

    private static final RowMapper<String> TEST_MAPPER = (rs,i) -> {
        String result = rs.getString("sub");
        return result;
    };

    public String findById(Integer id){
        String sql = "SELECT sub FROM sw WHERE id = :id;";
        SqlParameterSource param = new MapSqlParameterSource().addValue("id", id);
        List<String> subList = template.query(sql, param, TEST_MAPPER);
        if(subList.size() == 1){
            return subList.get(0);
        } else {
            return null;
        }   
    }
}

```

```java:SeleniumController

@Controller
@RequestMapping("/seleniumTest")
public class SeleniumController {

    @Autowired
    private SeleniumRepository repository;

    @RequestMapping("")
    public String index(){
        return "input";
    }

    @PostMapping("/judge")
    public String postMethodName(String intext, Model model) {

        String sub = "I have a bad feeling about this...";

        try {
            Integer id = Integer.parseInt(intext);
            String result = repository.findById(id);
            if(result != null){
                sub = result;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        model.addAttribute("sub", sub);
        return "result";
    }
}

```

#### データベース

|   id  |   sub   |
|-------|--------|
|1|The Phantom Menace|
|2|Attack of the Clones|
|3|Revenge of the Sith|
|4|A New Hope|
|5|The Empire Strikes Back|
|6|Return of the Jedi|
|7|The Force Awakens|
|8|The Last Jedi|
|9|The Rise of Skywalker|

テキストボックスに1〜９の数字を入れると対応する文字列を表示させます。
それ以外の数字、文字列だと「I have a bad feeling about this...」が表示されます。

#### テスト

こちらも、importは省略します。

pom.xmlに以下の依存関係を追加しました。

```xml:pom.xml
<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>4.24.0</version>
</dependency>
<dependency>
    <groupId>io.github.bonigarcia</groupId>
    <artifactId>webdrivermanager</artifactId>
    <version>5.9.2</version>
</dependency>
```

selenium-javaによってseleniumフレームワークが、webdrivermanagerによってブラウザのコントロールがJavaを通して行えるようになります。

```java:SeleniumTest

@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
public class SeleniumTest {

    private WebDriver driver;
    private Map<Integer ,String> param;

    @BeforeAll
    static void setUpBeforeAll(){
        System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, "/path/to/chromedriver-mac-arm64/chromedriver");
    }

    @BeforeEach
    void setUp(@Autowired JdbcTemplate template){
        driver = new ChromeDriver();

        param = new LinkedHashMap<>();
        param.put(1, "The Phantom Menace"); 
        param.put(2, "Attack of the Clones"); 
        param.put(3, "Revenge of the Sith");
        param.put(4, "A New Hope");
        param.put(5, "The Empire Strikes Back");
        param.put(6, "Return of the Jedi");
        param.put(7, "The Force Awakens");
        param.put(8, "The Last Jedi");
        param.put(9, "The Rise of Skywalker");

        String sql = "TRUNCATE sw RESTART IDENTITY CASCADE;";
        template.update(sql);

        sql ="INSERT INTO sw (sub) VALUES ('The Phantom Menace'), ('Attack of the Clones'), ('Revenge of the Sith'),('A New Hope'),('The Empire Strikes Back'),('Return of the Jedi'),('The Force Awakens'),('The Last Jedi'),('The Rise of Skywalker');";
        template.update(sql);
    }

    @AfterEach
    void tearDown(){
        if(driver != null){
            driver.quit();
        }
    }

    @ParameterizedTest
    @ValueSource(strings = {"1","2","3","4","5","6","7","8","9"})
    void サブタイトル表示_正常系(String value){
        driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(2));

        driver.get("http://localhost:8080/seleniumTest");

        WebElement element = driver.findElement(By.id("intext"));
        element.sendKeys(value);

        WebElement button = driver.findElement(By.id("sub-button"));
        button.click();
        
        String actual = driver.findElement(By.id("result")).getText();
        String expect = "sub-title : " + param.get(Integer.valueOf(value));

        assertEquals(expect, actual);
    }

    @ParameterizedTest
    @ValueSource(strings = {"10","0","-2","-","a"})
    void サブタイトル表示_異常系(String value){
        driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(2));

        driver.get("http://localhost:8080/seleniumTest");

        WebElement element = driver.findElement(By.id("intext"));
        element.sendKeys(value);

        WebElement button = driver.findElement(By.id("sub-button"));
        button.click();
        
        String actual = driver.findElement(By.id("result")).getText();
        String expect = "sub-title : I have a bad feeling about this...";

        assertEquals(expect, actual);
    }
}

```

#### Selenium関連のキーワード

- WebDriverインターフェース
  　Webドライバーに関連するメソッドが定義されています。new でインスタンスかできますが、その際に使用するブラウザの種類が選べます。今回はChromeを使用したため、`WebDriver driver = new ChromeDriver()`としました。
  　また、Chrome起動のためにChromeDriverのパス名を`System.setProperty(ChromeDriverService.CHROME_DRIVER_EXE_PROPERTY, "/path/to/chromedriver-mac-arm64/chromedriver");`で設定しました。

  - get()メソッド
    　引数にURLを渡すことで、そのページを開いてくれる。
  - manage()メソッド
    　メソッドチェーンでこの後にtimeoutやcookieの追加・削除の接待ができる。
  - findElement()・findElements()メソッド
    　idやクラス、xpathでの指定により、HTML内の要素を指定し、WebElementオブジェクトまたはListを返す。
  - quit()メソッド
    　ブラウザを閉じる。@AfterEachで指定。

<br>

- WebElementインターフェース
    　HTML内の要素に対応したオブジェクトとして扱うことができる。それぞれのタグに対応したアクションを起こすことができる。
  - sendKeys()メソッド
    　引数に指定した文字列を、テキストボックスやテキストエリアに入力する。
  - click()メソッド
    　visibleで幅と高さが０以上の要素をクリックできる。

https://www.seleniumqref.com/api/webdriver_abc_java.html

#### Selenium関連ではないけど、役に立ったキーワード

- @SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
    　これまではwebEnvironmentを指定していなかった。デフォルトではWebEnvironment.MOCKが設定されているため、@AutoConfigureMockMVCアノテーションかMockMVCBuilderでMockMVCを設定する必要があった。DEFINED_PORTを指定すると通常のspring起動（pom.xml通りの起動）をする。Mock化していないため、データベースに接続して値を取得することもできる。

- @ParameterizedTest,@ValueSource
    テストメソッドの引数に値を渡すことができる。valuesourceアノテーションを使用することで、配列から値を一つずつ渡していくことができる。他にも複数の引数を渡すことができる@CsvSourceなどもある。
    これでテストケースごとにメソッドを作る必要がなくなり、境界値検査もしやすくなりそう！

https://junit.org/junit5/docs/5.8.0/api/org.junit.jupiter.params/org/junit/jupiter/params/ParameterizedTest.html

テスト結果は以下のように表示されます。

## まとめ

ブラウザ操作もJavaからできるのは驚きでした。
HTMLのようも取得でき、単体テスト同様AssertEqualも使えて感動でした。