見出し画像

Playwrightに追加されたテストランナーでE2Eテストを書いてみた

こんにちは、みみぞうです。
ナビタイムジャパンで「システムや開発環境、チームの改善」を担当しています。

はじめに

PlaywrightはAPIを通してブラウザを自動操作するライブラリです。

MicrosoftがOSSとして開発しており、クロスプラットフォーム・クロスブラウザ対応されています。弊社でも今年に入ってから、プロダクトの検証に使いました。

当時のバージョンは導入にハードルがありました。テストをするために、テストランナーとして別のライブラリをインストールする必要があり、TypeScriptで使うにはいくつか設定が必要でした。

ところが、2021年6月にリリースされたv1.12でテストランナーが本体に同梱されました。また、特別な設定なしでもTypeScriptでテストコードが書けるようになり、公式ドキュメントにもTypeScriptのサンプルコードが掲載されるようになりました。

本記事では現時点の最新バージョンである Playwright v1.13 を使ってテストコードを書き終えるまでの過程をご紹介します。

準備

テスト対象のWebアプリを作成します。Webアプリは簡易的なものを用意しますが、フレームワークなどは実際のプロダクトで利用されることの多い技術を採用します。

TypeScript
Nuxt.js
Vuetify

Webアプリのプロジェクトをcreate-nuxt-appで作成します。

$ npx create-nuxt-app playwright-nuxt-note

✨  Generating Nuxt.js project in playwright-nuxt-note
? Project name: playwright-nuxt-note
? Programming language: TypeScript
? Package manager: Npm
? UI framework: Vuetify.js
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Linting tools: Prettier
? Testing framework: None
? Rendering mode: Single Page App
? Deployment target: Static (Static/Jamstack hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Continuous integration: None
? Version control system: Git

Nuxt Composition APIを使っているので、そちらも準備します。

npm i @nuxtjs/composition-api

nuxt.config.js の buildModulesに設定を追加します。

{
  buildModules: [
    '@nuxtjs/composition-api/module'
  ]
}

Webアプリのコードを書く

テストコードがメインのため、Webアプリのコードは layouts/default.vue に集約させます。

<template>
  <v-app dark>
    <v-layout column justify-center align-center>
      <v-container>
	 	<v-row align="center" justify="center">
          <h1 class="pa-15">演算アプリ</h1>
        </v-row>
        <v-form v-model="state.valid">
          <v-row align="center" justify="center">
            <v-col cols="12" md="1">
              <v-text-field
                v-model="state.x"
                type="number"
                :rules="xRules"
              ></v-text-field>
            </v-col>
            <v-col cols="12" md="1">
              <v-select
                v-model="state.operator"
                return-object
                item-text="operand"
                :items="operators"
              ></v-select>
            </v-col>
            <v-col cols="12" md="1">
              <v-text-field
                v-model="state.y"
                type="number"
                :rules="yRules"
              ></v-text-field>
            </v-col>
            <v-col cols="12" md="1">
              <span> = </span>
            </v-col>
            <v-col cols="12" md="1" type="number">
              <span v-text="result"> </span>
            </v-col>
          </v-row>

          <v-row align="center" justify="center">
            <v-btn :disabled="!state.valid" @click="handleReset">
              結果をリセットする
            </v-btn>
          </v-row>
        </v-form>
      </v-container>
    </v-layout>
  </v-app>
</template>

<script lang="ts">
import { computed, defineComponent, reactive } from '@nuxtjs/composition-api'

class Operator {
  private static readonly _values: Operator[] = []

  static readonly PLUS = new Operator('+', (x, y) => x + y)
  static readonly MINUS = new Operator('-', (x, y) => x - y)
  static readonly STAR = new Operator('*', (x, y) => x * y)

  private constructor(
    readonly operand: string,
    readonly operate: (x: number, y: number) => number
  ) {
    Operator._values.push(this)
  }

  static values(): Operator[] {
    return Operator._values
  }
}

interface State {
  x?: string
  y?: string
  operator: Operator
  valid: boolean
}

const defaultState: State = {
  x: '0',
  y: '0',
  operator: Operator.PLUS,
  valid: false,
} as const

export default defineComponent({
  setup() {
    const state = reactive<State>({ ...defaultState })

    const result = computed(() =>
      state.operator.operate(Number(state.x), Number(state.y))
    )

    const handleReset = () => {
      state.x = defaultState.x
      state.y = defaultState.y
      state.operator = defaultState.operator
    }

    return {
      state,
      result,
      handleReset,
      operators: Operator.values(),
      xRules: [(v: string | undefined) => !!v || 'x is required'],
      yRules: [(v: string | undefined) => !!v || 'y is required'],
    }
  },
})
</script>

起動して確認すると画面が表示されます。xとyと演算子を指定して結果を出力するシンプルなアプリケーションです。

npm run dev

画像1

テストランナーのインストール

ここからがテストの準備です。Gettings Startedでインストール方法が紹介されています。

@playwright/test をインストールします。

npm i -D @playwright/test

@playwright/test を使う場合、ブラウザは別途インストールが必要です。ここではchromiumだけインストールします。

npx playwright install chromium

テストコードを書く

テストファイルとして tests/layouts/default.spec.ts を作成します。

import { test, expect } from '@playwright/test'

test('画面が表示される', async ({ page }) => {
  await page.goto('http://localhost:3000')
  expect(await page.waitForSelector('text=演算アプリ')).toBeTruthy()
})

playwrightコマンドで実行します。

$ npx playwright test

Running 1 test using 1 worker

 ✓  tests\layouts\default.spec.ts:3:1 › 画面が表示される (5s)


 1 passed (5s)

Webアプリが起動していれば成功するはずです。

入力欄を操作して結果を確認する

2つ数を入力、演算子を選択して、結果が期待通りか確認するコードを書きます。何らかの手段で要素の特定が必要になります。Playwrightの Element Selector Best Practice を参考にします。

ベストプラクティスによると、要素の指定方法として以下3つが推奨されています。

・表示されているテキスト
・placeholderやlabelなど変更されにくいもの
・roleとテキストの組み合わせ

しかし、今回使うWebアプリのコードでは、いずれの方式でも指定できません。そのため、次点に推奨されているdata-test-idというテスト用の属性を付与します。(※ labelやplaceholderを追加すれば可能ですが、先ほど紹介したコードをそのまま使う場合は..という意味です)

data-test-idを4箇所に設定します。

         <v-form v-model="state.valid">
           <v-row align="center" justify="center">
             <v-col cols="12" md="1">
               <v-text-field
                 v-model="state.x"
                 type="number"
                 :rules="xRules"
                 data-test-id="x"
               ></v-text-field>
             </v-col>
             <v-col cols="12" md="1">
               <v-select
                 v-model="state.operator"
                 return-object
                 item-text="operand"
                 :items="operators"
                 data-test-id="operator"
               ></v-select>
             </v-col>
             <v-col cols="12" md="1">
               <v-text-field
                 v-model="state.y"
                 type="number"
                 :rules="yRules"
                 data-test-id="y"
               ></v-text-field>
             </v-col>
             <v-col cols="12" md="1">
               <span> = </span>
             </v-col>
             <v-col cols="12" md="1" type="number">
               <span v-text="result"> </span>
               <span v-text="result" data-test-id="result"> </span>
             </v-col>
           </v-row>
 
           <v-row align="center" justify="center">
             <v-btn :disabled="!state.valid" @click="handleReset">
               結果をリセットする
             </v-btn>
           </v-row>
         </v-form>

data-test-idによる要素は、'data-test-id=xxx' と示せます。

tests/layouts/default.spec.ts に足し算のテストを追加します。

import { test, expect } from '@playwright/test'

test.beforeEach(async ({ page }) => {
  await page.goto('http://localhost:3000')
})

test('画面が表示される', async ({ page }) => {
  expect(await page.waitForSelector('text=演算アプリ')).toBeTruthy()
})

test('足し算ができる', async ({ page }) => {
  // 左側の<v-text-field>に5を入力
  await page.fill('data-test-id=x', '5')

  // <v-select>で+を選択
  await page.click('data-test-id=operator')
  await page.click('[class=v-list-item__title] >> text=+')

  // 右側の<v-text-field>に10を入力
  await page.fill('data-test-id=y', '10')

  // 結果が15になることを確認
  expect(await page.innerText('data-test-id=result')).toBe('15')
})

実行して成功すればOKです。なお、--headedフラグを付けるとブラウザの操作が表示されます。自動操作の様子を目で確認したいときは便利です。

$ npx playwright test --headed

Running 2 tests using 1 worker

 ✓  tests\layouts\default.spec.ts:7:1 › 画面が表示される (3s)
 ✓  tests\layouts\default.spec.ts:11:1 › 足し算ができる (3s)


 2 passed (6s)

テストコードを階層化する

足し算以外のテストケースも追加したらテストコードが増えてきました。test.describeで階層化することによって視認性を上げます。

tests/layouts/default.spec.ts を編集します。

import { test, expect } from '@playwright/test'

test.describe('default.vue', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('http://localhost:3000')
  })

  test('画面が表示される', async ({ page }) => {
    expect(await page.waitForSelector('text=演算アプリ')).toBeTruthy()
  })

  test('足し算ができる', async ({ page }) => {
    await page.fill('data-test-id=x', '5')

    await page.click('data-test-id=operator')
    await page.click('[class=v-list-item__title] >> text=+')

    await page.fill('data-test-id=y', '10')

    expect(await page.innerText('data-test-id=result')).toBe('15')
  })

  test('引き算ができる', async ({ page }) => {
    await page.fill('data-test-id=x', '5')

    await page.click('data-test-id=operator')
    await page.click('[class=v-list-item__title] >> text=-')

    await page.fill('data-test-id=y', '10')

    expect(await page.innerText('data-test-id=result')).toBe('-5')
  })

  test('かけ算ができる', async ({ page }) => {
    await page.fill('data-test-id=x', '5')

    await page.click('data-test-id=operator')
    await page.click('[class=v-list-item__title] >> text=*')

    await page.fill('data-test-id=y', '10')

    expect(await page.textContent('data-test-id=result')).toBe('50')
  })
})

再び実行します。ホンモノのテストみたいになってきましたね。

$ npx playwright test --headed

Running 4 tests using 1 worker

 ✓  tests\layouts\default.spec.ts:8:3default.vue 画面が表示される (3s)
 ✓  tests\layouts\default.spec.ts:12:3default.vue 足し算ができる (3s)
 ✓  tests\layouts\default.spec.ts:23:3default.vue 引き算ができる (3s)
 ✓  tests\layouts\default.spec.ts:34:3default.vue かけ算ができる (3s)


 4 passed (12s)

実装への依存を最小限にする

先ほどのコードを見直してみましょう。

  test('引き算ができる', async ({ page }) => {
    await page.fill('data-test-id=x', '5')

    await page.click('data-test-id=operator')
    await page.click('[class=v-list-item__title] >> text=-')

    await page.fill('data-test-id=y', '10')

    expect(await page.innerText('data-test-id=result')).toBe('-5')
  })

テストを書いた本人以外の人が見たとき、初見で何をしようとしているのか分かりにくいですね。特に演算子の選択は、『演算子に-を選択』したいだけなのに暗号のように見えます。そこで、Page Object Modelというパターンを用いてコードを書き直します。

Page Object Modelでは画面のヘルパーを作り、やりたいことを抽象化したメソッドを提供します。テストコードなので、チームによっては英語以外の言語を利用した方が捗るケースもあります。試しに日本語で書いてみます。

tests/page-helper/default-page.ts を作成します。

import { expect, Page } from '@playwright/test'

export class DefaultPage {
  constructor(public readonly page: Page) {}

  async テキストがどこかに表示されることを確認(text: string) {
    expect(await this.page.waitForSelector(`text=${text}`)).toBeTruthy()
  }

  async 結果の確認(result: string) {
    expect(await this.page.textContent('data-test-id=result')).toBe(result)
  }

  async xに入力(x: string) {
    await this.page.fill('data-test-id=x', x)
  }

  async yに入力(y: string) {
    await this.page.fill('data-test-id=y', y)
  }

  async オペレーターを選択(operand: '+' | '-' | '*') {
    await this.page.click('data-test-id=operator')
    await this.page.click(`[class=v-list-item__title] >> text=${operand}`)
  }
}

Page Object Modelを使ったテストコードに tests/layouts/default.spec.ts を書き直してみましょう。

import { test as base } from '@playwright/test'
import { DefaultPage } from '../page-helper/default-page'

const test = base.extend<{ defaultPage: DefaultPage }>({
  defaultPage: async ({ page }, use) => {
    const defaultPage = new DefaultPage(page)
    await page.goto('http://localhost:3000')
    await use(defaultPage)
  },
})

test.describe('default.vue', () => {
  test('画面が表示される', async ({ defaultPage }) => {
    await defaultPage.テキストがどこかに表示されることを確認('演算アプリ')
  })

  test('足し算ができる', async ({ defaultPage }) => {
    await defaultPage.xに入力('5')
    await defaultPage.オペレーターを選択('+')
    await defaultPage.yに入力('10')

    await defaultPage.結果の確認('15')
  })

  test('引き算ができる', async ({ defaultPage }) => {
    await defaultPage.xに入力('3')
    await defaultPage.オペレーターを選択('-')
    await defaultPage.yに入力('7')

    await defaultPage.結果の確認('-4')
  })

  test('かけ算ができる', async ({ defaultPage }) => {
    await defaultPage.xに入力('3')
    await defaultPage.オペレーターを選択('*')
    await defaultPage.yに入力('-3')

    await defaultPage.結果の確認('-9')
  })
})

前のコードと比べて、何をテストしたいのか分かりやすくなったのではないでしょうか。Vuetifyの仕様が変わり、セレクタが変わってしまった場合でもPage Object Modelのみ修正すればいいので安心ですね。

説明しませんでしたが、testの中で{ defaultPage }を受け取れるのはfixturesという機能によるものです。

letbeforeEachafterEachと比べて処理が一箇所に集まるのでコードが見やすくなります。

まとめ

Playwright v1.12 から追加されたテストランナーを使ってWebアプリのE2Eテストを書いてみました。

今回の記事では紹介しませんでしたが、Playwrightには他にも便利な機能が沢山あります。

たとえば、スクリーンショットを比較するVisual comparsions

テストの様子をGUIツールで細かく観察できるTrace Viewer

これらの機能は公式ドキュメントに詳しく紹介されています。スマートにメンテナンスできるよう、必要な機能をキャッチアップしてプロダクトの品質を高めていければと思っています。

最後までお読みいただきありがとうございました..!! 😄