はじめに
「 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
なので「X
がY
である」ことを検証します。 他にも、expect(X).not.toBe(Y)
で「X
がY
でない」、expect(X).toContain(Y)
で「X
にY
が含まれている」など、toBe
の部分を変化させることで色々な検証ができます。
操作系
ページにアクセスする
E2Eテストではページにアクセスしないと始まりませんね。ページにアクセスする場合はpage.goto()
を使います。 例えばhttp://localhost:3000
にアクセスしたい場合は、次のように書きます。
await page.goto('http://localhost:3000')
ボタンやリンクをクリックする
ボタンやリンクなどの要素をクリックするときはpage.click()
を使います。
例えば、次のボタン要素があるとします。
<button id="hoge">Click me!!</button>
このボタンをクリックする場合、id
属性を利用して次のように書きます。
await page.click('#hoge')
入力フォームに文字を入力する
input
やtextarea
に文字入力をします。
例えば、次のテキストフィールドがあるとします。
<input type="text" id="hoge" />
このテキストフィールドに「こんにちは」と入力する場合、id
属性を利用して次のように書きます。
await page.type('#hoge', 'こんにちは')
ちなみに、すでに文字が入力されている場合、入力済みの値に文字が追加されるので注意が必要です。
await page.type('#hoge', 'こんにちは') // 「こんにちは」と入力されている状態になるawait page.type('#hoge', 'こんばんは') // 「こんにちはこんばんは」と入力されている状態になる
入力フォームの文字を消す
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属性がhoge
のinput
の入力文字を消すことができます。
入力フォームの文字を消して、新しい文字を入力する
上の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()
で画面遷移やリロードを待つことができます。
画面に要素が現れるまで待つ
例えば、次のボタン要素があるとします。
<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')
とりあえず操作したい
とりあえずブラウザを操作したい場合は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')
要素が存在することを検証する
<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()
で検証します。
要素の属性を検証する
例えば、次の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
に#hoge
、pageFunction
にel => el.src
としているので、#hoge
のimg
のsrc
を取得してます。
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()
でimg
classの要素の配列を取得して、後続のfunctionにels
として流してます。 次に、map
を使って配列の1要素ずつのsrc
を取り出し、srcs
に返却しています。 srcs
はimg
classの要素の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
では次のようにメタタグの定義します。
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
おわりに
テストコードを書く機会があったので、よく使いそうだなというコードをユースケースドリブンでまとめてみました。 色々なサイトを巡りましたが、最終的にはソースに落ち着く、ということで次のページをとても参考にしました。
今後も新しいテストを書いたら追加していきます。
サポートもお待ちしております!