CodeceptJSで超簡単なE2Eテストを書いてみた

# 前提
保守を行っているプロダクトでE2Eテストの仕組みを作っています。
まったくノウハウのない状態からのスタートのため、E2Eテストのスペシャリストの方に協力していただきながら、まずはテスト実装を進め、データ管理の仕組みやテストコードの管理方法などを検討しています。

今回の記事ではそのテスト実装で学習した、E2Eテストを記述する方法をまとめます。
以下のような前提のもと書いています。

- CodeceptJS + Playwright
- テストシナリオはgherkinパラメーターで記述
- 実装はTypeScript
- `Page Object Model Design Pattern` でtsファイルを記述する構成
- 実行するために必要なセットアップは済んでいる
- 超簡単なアプリケーションに対して超簡単なテストシナリオを記述する


Codecept.conf.ts
```TypeScript
export const config: CodeceptJS.MainConfig = {
  gherkin: {
    features: './src/scenario/**/*.feature',
    steps: './src/scenario/**/*.ts',
  }
}
```

# 各ファイルの役割
実行の準備が整っている前提で、この構成でテストを書くのに必要なファイルは以下の３種類。

## Featureファイル
テストシナリオの塊。
テスト実行時はこの中のテストシナリオ単位で実行する。
Gherkin記法でテストシナリオを書き、ステップ記述用のtsファイルを呼び出す。

## ステップ記述用のtsファイル
Featureファイルから呼び出され、ページオブジェクト用のtsファイルを利用してテストの流れを記述する。

## ページオブジェクト用のtsファイル
ページごとに分けて作成する。（ログインページ、メインページなど）
ページ内のオブジェクトの定義と直接的なオブジェクトの操作を記述する。

# 実装例
以下のようなWebアプリがある場合

- ログインページからログインするとメインページに遷移する
- メインページの中にはユーザー情報を表示するボタンがある
- ユーザー情報を表示するボタンを押すとユーザーIDが表示される

ユーザーIDが正しく表示されることをテストするE2Eテストを書く場合のそれぞれのファイルがどうなるか。


## featureファイル

USERINFO_CHECK.feature

```Gherkin
Feature: メインページユーザー情報
    Scenario: ユーザー情報にログインしたユーザーIDが表示される
        Given 管理者アカウント "admin" でログインする。パスワードは "admin"
        When ユーザー情報のボタンをクリックする
        Then アカウント名 "admin" が表示される
```

## ステップ記述用のtsファイル
USERINFO_CHECK.ts
```TypeScript
import { 
    LoginPage,
    MainPage
} from '../../pages'; // pageオブジェクト用のtsファイルが置いてあるフォルダ

const { I } = inject();

Given('管理者アカウント{string}でログインする。パスワードは{string}', async (userId: string, password: string) => {   
    const loginPage = new LoginPage();
    await loginPage.login(userId, password);
});

When('ユーザー情報のボタンをクリックする', async () => {
    const mainPage = new MainPage();
    await mainPage.clickUserInfoButton();
});

Then('アカウント名{string}が表示される', async (userId: string) => {
    const mainPage = new MainPage();
    mainPage.assertUserId(userId);
});
```

## ページオブジェクト用のtsファイル
loginPage.ts
```TypeScript
const { I } = inject();

// ログイン画面
export class LoginPage {

    readonly LOCATOR = {

        // ユーザーIDのEdit
        inputUserId: 'ユーザーID', // プレースホルダに入っている

        // パスワードのEdit
        inputPassword: 'パスワード',// プレースホルダに入っている

        // ログインボタン
        buttonLogin: 'ログイン',// ボタンのキャプション
    }

    constructor() {
    } 

    // ログイン操作を行う
    async login(userId: string, password: string) {
        // ログイン画面に遷移
        I.amOnPage('http://hoge.com:8080/fuga/login');
        
        //ユーザーIDとパスワードを入力
        I.fillField(this.LOCATOR.inputUserId, userId);
        I.fillField(this.LOCATOR.inputPassword, password);
        
        //ログインボタンをクリックする
        I.click(this.LOCATOR.buttonLogin);

        return this;
    }
}
```

mainPage.ts
```TypeScript
const { I } = inject();

// メインページ
export class MainPage {

    readonly LOCATOR = {
        // ユーザー情報を表示するボタン
        buttonUserInfo: '#userinfo-button', //DOMのid
        // ユーザーIDを表示するラベル
        labelUserId: '#userid-label', //DOMのid
    }
    
    constructor() {
    }

    // ユーザー情報ボタンをクリックする
    async clickButtonUserInfo() {
        I.click(this.LOCATOR.buttonUserInfo);
    }

    // ユーザー情報にログインユーザーのIDが表示されていることをアサーション
    async assertUserId(userId: string) {
        I.seeTextEquals(userId, locate(this.LOCATOR.labelUserId));
        return new ConfigPage();
    }

}


```

## 実行コマンド
```
npx codeceptjs run --grep ユーザー情報にログインしたユーザーIDが表示される
```

# 感想
- GherkinのシナリオとTypeScriptのファイルを分離することでシナリオと実装が分離されていて良い
- さらにページオブジェクトを分けることでより明確に分離できて良い
- ページオブジェクトを操作する実装を分けることでシナリオが増えた時に共通化できそう
- CodeceptJSが賢い
    - Locaterの定義がIDやnameだけでなくプレースホルダやキャプションでもできて楽
    - 細かくwaitを書かなくても描画を待ってくれる