2021年2月28日

Jest + PuppeteerのE2Eテストで使うコード集

読了時間の目安: 9分


はじめに

Nuxt on Dockerにて、Jest + Puppeteer でE2Eテスト環境をセットアップする 」のブログで、Jest + Puppeteerを使ったE2Eテスト環境を整えました。 この記事では、実際にE2Eテストを記述するときに使う操作や検証のコードをまとめておきます。 自分が使ったコードを書いていくので、順次増えていく予定。

基本

テストファイル内の基本構文は次のとおりです。

describe('テストスイート名', () => {
test('テストケース名', async () => {
await page.goto('http://localhost:3000') // 操作とか
await expect(page.url()).toBe('http://localhost:3000') // 検証とか
})
})

describeとtest

1つのファイルには1つのdescribeがあります。そして、1つのdescribeの中に複数のtestを記述できます。 testはテストケース、describeはテストスイート(テストケースを目的別などでまとめたもの)といえます。

test内は複数の操作と検証から成り立ちます。これらはasync/awaitを使って直列に処理させます。 例の場合、await page.goto('http://localhost:3000')で「http://localhost:3000にアクセスする」操作をしています。 そして、await expect(page.url()).toBe('http://localhost:3000')で「表示中のページのURL期待通りである」検証しています。

describe(name, fn)
test(name, fn, timeout)

expect(X).toBe(Y)

検証はexpect(X).toBe(Y)を使うことが多いです。toBeなので「XYである」ことを検証します。 他にも、expect(X).not.toBe(Y)で「XYでない」、expect(X).toContain(Y)で「XYが含まれている」など、toBeの部分を変化させることで色々な検証ができます。

expectのメソッド

操作系

ページにアクセスする

E2Eテストではページにアクセスしないと始まりませんね。ページにアクセスする場合はpage.goto()を使います。 例えばhttp://localhost:3000にアクセスしたい場合は、次のように書きます。

await page.goto('http://localhost:3000')

page.goto(url)

ボタンやリンクをクリックする

ボタンやリンクなどの要素をクリックするときはpage.click()を使います。
例えば、次のボタン要素があるとします。

<button id="hoge">Click me!!</button>

このボタンをクリックする場合、id属性を利用して次のように書きます。

await page.click('#hoge')

page.click(selector)

入力フォームに文字を入力する

inputtextareaに文字入力をします。
例えば、次のテキストフィールドがあるとします。

<input type="text" id="hoge" />

このテキストフィールドに「こんにちは」と入力する場合、id属性を利用して次のように書きます。

await page.type('#hoge', 'こんにちは')

ちなみに、すでに文字が入力されている場合、入力済みの値に文字が追加されるので注意が必要です。

await page.type('#hoge', 'こんにちは') // 「こんにちは」と入力されている状態になる
await page.type('#hoge', 'こんばんは') // 「こんにちはこんばんは」と入力されている状態になる

page.type(selector, text)

入力フォームの文字を消す

page.type()は入力済みの文字列に追加されるので、新しく文字を入力したい場合は一度フォームの入力値をクリアする必要があります。 await page.type('#hoge', '')などでクリアできればいいのですが、そうはいきません。

<input type="text" id="hoge" value="Hello" />

のようにすでに「Hello」が入力されているテキストフィールドの入力値を消す場合、例えば次のようなコードで実現できます。

const el = await page.$('#hoge')
await el.click({ clickCount: 3 })
await el.press('Backspace')

page.$()selectorに合致する要素を取得し、それを3回クリックし、Backspaceを押す、という操作です。 実際にブラウザでテキストフィールド(内の文字列)を3回クリックすると、入力されている文字列が全選択の状態になり、そこでBackspaceを押すと文字が全部消えますよね。 このようにブラウザで実際に行う操作をコーディングすることで入力フォームの文字をクリアできます。

最初に関数定義しておくとより使いやすいです。

const clearInput = async (id) => {
const el = await page.$(id)
await el.click({ clickCount: 3 })
await el.press('Backspace')
}
test('hoge' async () => {
...
clearInput('#hoge')
})

これで、clearInput('#hoge')でid属性がhogeinputの入力文字を消すことができます。

page.$(selector)

入力フォームの文字を消して、新しい文字を入力する

上の2つを組み合わせれば、すでに文字が入力されているinputに新しい文字列を1列で入力できる関数もつくれます。

const newInput = async (id, value) => {
const el = await page.$(id)
await el.click({ clickCount })
await el.press('Backspace')
await page.type(id, value)
}
test('hoge', async () => {
...
await newInput('#hoge', 'hogehoge')
...
})

よく使うので、関数化しておくと便利です。

画面遷移やリロードを待つ

await page.waitForNavigation()

で画面遷移やリロードを待つことができます。

page.waitForNavigation()

画面に要素が現れるまで待つ

例えば、次のボタン要素があるとします。

<button id="hoge">Click me!!</button>

このボタンがなにかの処理が終わってから表示されるとします。このとき、ボタンが表示される前にpage.click()などをしてしまうと、要素が見つからないためエラーになってしまいます。 そこで、次のようにして要素が現れるのを待ちます。

await page.waitForSelector('#hoge')

もし、ボタンが現れない場合はテストがタイムアウトで失敗になるので注意です。

page.waitForSelector(selector)

画面から要素が消えるまで待つ

例えば、次のボタン要素があり、一度クリックすると要素が消えるとします。

<button id="hoge">押したら消えるよ</button>

画面から要素が消えるまで操作を待ちたい場合、次のように書きます。

await page.waitForSelector('#hoge', { hidden: true })

画面に要素が現れるまで待つpage.waitForSelector()hiddenオプションを有効にすることで、「消えるまで待つ」操作になります。

page.waitForSelector(selector)

指定時間待つ

時間を指定して操作や検証を待ちたいことがあります。例えば1秒待ちたい場合は次のコードで実現できます。

await page.waitForTimeout(1000)

()の単位はミリ秒なので、1秒の場合は1000です。

page.waitForTimeout(milliseconds)

フォーカスを外したい

inputに入力している状態からフォーカスを外す操作はblurを使えばできます。

await page.$eval('#hoge', el => el.blur())

とりあえずキーボード操作したい

要素関係なく矢印を押してみたり、Enterを押してみたり、というケースもあります。 その場合はpage.keyboardが便利です。例えば「右矢印(→)」を押したい場合、次のとおりです。

await page.keyboard.press('ArrowRight')

class: KeyBoard

とりあえず操作したい

とりあえずブラウザを操作したい場合はpage.evaluate(pageFunction)を使います。 例えば、ページをスクロールしたいときはwindow.scrollTo()を使うとできます。スクロールすると出現する要素、とか検証するときに使います。

await page.evaluate(() => {
window.scrollTo({ top: 1000 })
})

page.evaluate(pageFunction)
window.scrollTo

検証系

表示中のページのURLを検証する

表示中のページのURLはpage.url()で取得できます。これが期待値(例:http://localhost:3000)かどうかは次のように検証できます。

await expect(page.url()).toBe('http://localhost:3000')

page.url()

要素が存在することを検証する

<p id="hoge">Show me!!</p>

の要素がページに表示されているかどうかは次のように検証できます。

await expect(page.$('#hoge')).not.toBeNull()

page.$()は要素が見つからない場合nullを返します。なので「Nullであること」を検証する.toBeNull()に否定の.notをつけることで「Nullでないこと」を確認し、要素が存在することを検証できます。

page.$(selector)
.not
.toBeNull()

要素が存在しないことを検証する

<p id="hoge" style="display: none;">見えない</p>

の要素が存在しない(表示されていない)ことを検証します。

await expect(page.$('#hoge')).toBeNull()

page.$()は要素が見つからない場合nullを返すので、toBeNull()で検証します。

page.$(selector)
.toBeNull()

要素の属性を検証する

例えば、次のimg要素があるとします。

<img id="hoge" src="./images/hoge.png" />

このimg要素のsrcに正しい値が設定されているか検証したいとします。 この場合、page.$eval()を使ってsrc属性を取得して検証できます。

const src = await page.$eval('#hoge', el => el.src)
await expect(src).toBe('./images/hoge.png')

page.$eval(selector, pageFunction)selectorで取得した要素に対してpageFunctionを返します。例の場合はselector#hogepageFunctionel => el.srcとしているので、#hogeimgsrcを取得してます。

page.$eval(selector, pageFunction)

要素のテキストを検証する

例えば、次のparagraph要素があるとします。

<p id="greeting">こんにちは!</p>

このparagraph要素のテキストが意図通りか検証したいとします。

const text = await page.$eval('#greeting', el => el.innerText)
await expect(text).toBe('こんにちは!')

要素の属性の検証と似ていますが、innerTextを用いてelの中のテキストを抜いてくる方法があります。

page.$eval(selector, pageFunction)
HTMLElement.innerText

inputの入力値を検証する

これも要素の属性検証の応用です。

<input id="hoge" type="text" />

のようなテキストフィールドに「こんにちは」と入力して、「こんにちは」と入力されたかを検証する、といった流れです。 inputの入力値はvalue属性になるので、それを拾ってくればOKです。

await page.type('#hoge', 'こんにちは')
await expect(await page.$eval('#hoge', el => el.value)).toBe('こんにちは')

page.type(selector, text)
page.$eval(selector, pageFunction)
HTMLInputElement

buttonが非活性かを検証する

これも要素の属性検証の応用です。

<button id="hoge" disabled>押せないよ</button>

のような非活性なボタンを、ちゃんと非活性になっているか検証します。

await expect(await page.$eval('#hoge', el => el.disabled)).toBe(true)

HTMLButtonElementはTrue or Falseのdisabledを持っているのでそれで検証します。 逆にボタンが活性状態かは以下で検証できます。

await expect(await page.$eval('#hoge', el => el.disabled)).toBe(false)

page.$eval(selector, pageFunction)
HTMLButtonElement

複数の要素の属性を検証する

例えば、次のように複数のimg要素があるとします。

<img class="img" src="./images/hoge.png" />
<img class="img" src="./images/fuga.png" />

この2つのimg要素のsrcがそれぞれ意図通りかを調べたいとします。

const srcs = page.$$eval('.img', els => els.map(el => el.src))
await expect(src[0]).toBe('./images/hoge.png')
await expect(src[1]).toBe('./images/fuga.png')

まず、page.$$rval()imgclassの要素の配列を取得して、後続のfunctionにelsとして流してます。 次に、mapを使って配列の1要素ずつのsrcを取り出し、srcsに返却しています。 srcsimgclassの要素のsrc属性が順番に並んだ配列なので、src[index]で順番を指定して検証ができます。

page.$$eval(selector, pageFunction)
Array.prototype.map()

target="_blank"の別タブで開くを検証する

<a id="link_google" href="https://www.google.com/" target="_blank">Google</a>

のように別タブで遷移した場合、新しく開いたタブを検証したいときがあります。 検索するとbrowser.once('targetcreated', function)を使ってやる例が多かったのですが、やりたいことができず直感的でない感じもしたため、次のやり方で検証してます。

await page.click('#link_google')
await page.waitForTimeout(2000)
const pages = await browser.pages()
const newPage = pages[pages.length - 1]
await expect(newPage.url()).toBe('https://www.google.com/')

少しダサいですが、リンククリック後、タブの準備が整うまでpage.waitForTimeout()でスリープを入れています。 その後、browser.page()で全てのタブを取得し、pages[pages.length - 1]で一番うしろのタブ、つまり今新しく開かれたタブをnewPageに入れてます。

page.click(selector)
page.waitForTimeout(milliseconds)
browser.pages()
page.url()

meta tagsを検証する

メタタグも要素の1つですので、他の要素と同じように検証できます。 例えば、Nuxt.jsの場合、nuxt.config.jsでは次のようにメタタグの定義します。

nuxt.config.js
export default {
head: {
meta: [
{ hid: 'og:site_name', property: 'og:site_name', content: 'MyApp' }
]
}
}

これは次のようにHTMLに変換されるため、同様に扱うことができます。

<head>
<mata data-n-head="ssr" data-hid="og:site_name" property="og:site_name" content="MyApp">
</head>
await expect(await page.$eval('meta[property="og:site_name"]', el => el.content)).toBe('MyApp')

で検証が可能です。

page.$$eval(selector, pageFunction)
メタタグと SEO - NuxtJS

おわりに

テストコードを書く機会があったので、よく使いそうだなというコードをユースケースドリブンでまとめてみました。 色々なサイトを巡りましたが、最終的にはソースに落ち着く、ということで次のページをとても参考にしました。

今後も新しいテストを書いたら追加していきます。

気に入っていただけたら、サポートもお待ちしております!
  • 名前:asato
  • 仕事:スクラムマスター
  • 好き:家族、温泉、旅行、謎解き
  • 苦手:はじめまして、あんこ、うなぎ