はじめに

Webテストシリーズの以前の記事

Nightwatch Webテストの基礎
要素の検索、操作、プロパティの検証という3つのテクニックを使用して、Web上のほとんどのシナリオをテストする方法を学びましょう。

前回の記事では、この3つの強力なテクニックを使用してWebの基本的なシナリオをテストする方法を学びました。

  • 要素の検索 →  browser.element.find()
  • 要素とのインタラクション → .click().sendKeys()
  • 要素の検証 → .getText().assert.contains()

この記事の概要

今日は、Webテストにおける高度なテクニックとユースケースについて学びます。これにより、Nightwatchの幅広い機能を理解するのに役立ちます。次のシナリオと概念を紹介します。

概念

  • テストフック
  • キーボードショートカットとクリップボード
  • OSとブラウザ情報
  • クライアントサイドJSの実行
  • Actions API
  • iFrame
  • Async/Await
  • マルチタブ操作
  • ジオロケーションのエミュレート

以下のテストは、それぞれ記述した後に個別に実行することも、最後にまとめて実行することもできます。また、同様のシナリオに遭遇した際に参照としてこの記事を使用することもできます。

複雑なシナリオと概念

🪝 テストフック

テストフックは、テスト実行プロセスのさまざまな段階で特定のアクションを実行できるようにする特別な関数です。`home.spec.js`テストでは、常にNightwatch Webサイトのホームページにアクセスする必要があります。各テストで`browser.navigateTo('/')`を記述する代わりに、各テストの実行前に実行される`beforeEach`フックに追加できます。また、各テスト後に`browser.end()`を使用してブラウザを閉じることができます。

describe('Nighwatch homepage', function() {
  beforeEach(browser => browser.window.maximize().navigateTo('/'))
  afterEach(browser => browser.end())
  ...
})

ファイル(`home.spec.js`)からテストを開始する前または後に何かを実行する場合は、`before`および`after`フックを使用して実行できます。グローバルフックを使用して、テストランナーの開始前または終了直前に操作を実行することもできます。

💡
.window.maximize() はブラウザウィンドウを画面サイズに最大化します。

📎 キーボードショートカットとクリップボード

以前の記事こちら.sendKeys()を使用したキー押下のシミュレーション方法を学びましたが、コピーとペーストを含むショートカットも同様に簡単です。CONTROLまたはCOMMANDまたはSHIFTを押すと、そのキーはKeys.NULLが押されるまで保持され、押されているすべてのキーが解放されます。しかし、クリップボードは直接アクセスしてテストすることはできません。キーボードショートカット( + v / Ctrl + v)を使用して入力要素に貼り付け、入力のvalue属性をチェックすることで、クリップボードの内容を確認します。次のシナリオをテストしてみましょう。

ホームページの「コピー」ボタンをクリックし、テキストがコピーされたかどうかを確認します。

it('Should copy the installation command on copy button click', function (browser) {
  browser.element.findByText('Copy').click()
  browser.element.find('#docsearch').click()
  const $inputEl = browser.element.find('.DocSearch-Modal .DocSearch-Form input')
  $inputEl.sendKeys([browser.Keys.COMMAND, 'v'])
  $inputEl.getAttribute('value').assert.contains('npm init nightwatch')
})

WindowsまたはLinuxでテストを実行する場合は、browser.Keys.COMMANDbrowser.Keys.CONTROLに置き換える必要があります。これが次のトピックにつながります。

🌐 OSとブラウザ情報

テストが実行されているブラウザとプラットフォームに関する詳細は、browser.capabilitiesオブジェクトから取得できます。最も一般的に使用されるのは、.platformName.browserName.browserVersionの3つです。以前の例で使用し、複数のプラットフォームで実行するようにテストを書き直すことができます。

const is_mac = browser.capabilities.platformName.toLowerCase().includes('mac')
...
$inputEl.sendKeys([is_mac ? browser.Keys.COMMAND : browser.Keys.CONTROL, 'v'])

🤝 クライアントサイドJSの実行

次に、ブラウザクライアントでいくつかのJSを実行してみます。Nightwatchでは、executeScriptまたはexecuteAsyncScript関数を使用してこれを行うことができます。

💡
.executeScript(<script>, [...<data>], <optional-return-function>)

<script>引数は、文字列`"window.location.reload()"`または関数`function (...<args>) {...}`にすることができます。

スクリプト関数の引数...<args>は、送信された[...<data>]になります。

<optional-return-function>には、クライアントスクリプトの戻り値が引数として渡されます。

ロゴのサイズを大きくし、「Get Started」ボタンのテキストを「{Client Side Execution}」に変更するクライアントスクリプトを実行します。新しいテキストを持つボタンを見つけ、クリックしてみてください。

it('Should should change with client script', async function (browser) {
  const change_text = "{Client Side Execution}"
  browser.executeScript(function (new_text) {
    const $hero_cta = document.querySelector('.hero__action-button--get-started')
    $hero_cta.innerHTML = new_text
    $hero_cta.style.background = '#ff7f2b'
    document.querySelector('header .navigation-list').style.display = 'none'
    document.querySelector('header .navigation__logo').style.width = '900px'
  }, [change_text])
  browser.pause(1000) // Pausing just to notice the changes
  browser.element.findByText(change_text).click()
  browser.assert.titleMatches('Getting Started')
})

✍ ️Actions API

Actions APIは、指定された入力デバイスが正確に何ができるかを細かく制御できます。Nightwatchは、3種類の入力ソースのインターフェースを提供します。

  • キーボードデバイスのキー入力
  • マウス、ペン、またはタッチデバイスのポインター入力
  • スクロールホイールデバイスのホイール入力

これは既存の.perform()コマンド内で行うことができます。利用可能なアクションは、.clear().click([element]).contextClick([element]).doubleClick([element]).dragAndDrop(from, to).insert(device, ...actions).keyDown(key).keyUp(key).keyboard().mouse().move([options]).pause(duration, ...devices).press([button]).release([button]).sendKeys(...keys).synchronize(...devices)です。

browser
  .perform(function () {
    const actions = this.actions({ async: true })

    return actions
      .keyDown(Keys.SHIFT)
      .move({ origin: el })
      .press()
      .release()
      .keyUp(Keys.SHIFT)
  })

🖼 iFrame

Webの最も難しい側面の1つはiFrameです。これらの埋め込まれたiFrameは完全に異なるWebページをレンダリングし、独自のブラウジングコンテキストとドキュメントを持ち、独自の非継承Webページを内部に持つことができます。Nightwatchは、.frame()メソッドを使用してこれらのドキュメントに切り替える方法を提供しています。

💡
.frame(<identifier>)
そして<identifier>は次のいずれかになります。
- id: ターゲットとするiframeのid属性
- number: ドキュメント内のiframeの位置。0から始まります。
- null: 元のブラウザウィンドウに切り替えるために使用します。

iframe内でメールアドレスを入力し、「購読」をクリックします。

it('Should allow for substack subscription', function (browser) {
  const iframe_selector = '.footer__wrapper-inner-social-subscribe iframe'
  browser
    .executeScript(function (iframe_selector) {
      document.querySelector(iframe_selector).scrollIntoView()
    }, [iframe_selector])
  browser.element.find(iframe_selector).setAttribute('id', 'test-nightwatch-123')
  browser.frame('test-nightwatch-123')

  browser.element.find('input[type=email]').sendKeys('test@nightwatchjs.org')
  browser.element.find('button[type=submit]').click()

  browser.ensure.alertIsPresent()
  browser.alerts.accept()
  browser.element.findByText('Sign out').assert.present()
})
💡
.setAttribute(<element>, <attribute>, <new-value>) を使用すると、DOM属性に新しい値を設定できます。(詳しくはこちら

.ensure.assertに似ており、柔軟性を高めています。(詳しくはこちら

.alerts.[accept/dismiss/getText/setText]() を使用して、ブラウザの警告ボックスと対話できます。(詳しくはこちら

操作するiframeは遅延読み込みされるため、ページの一番下までスクロールするクライアントスクリプトを実行します。このページの一番下へのスクロールは、Actions APIを使用して次のように行うこともできます。

browser
  .perform(function() {
    return this.actions().move({
      origin: browser.element.find(iframe_selector),
    })
  })

🚦 Async/Awaitを使わない(場合もある)

JavaScriptは、ユーザーインターフェースの非同期的な性質に基づいて構築された言語です。これは開発者にとって悩みの種であり、コードの実行順序を制御するために、コールバックを使用し始めました。しかしすぐに、コールバック地獄に陥り、嫌気がさしました。その後、Promiseの素晴らしさが導入され、すぐにES7でその構文糖としてasync/awaitが導入されました。

JavaScriptでテストを作成する場合、多くの場合、「このコードはなぜ実行されないのか」、「この行は現時点では実行されるべきではない」という同じ問題に遭遇します。Nightwatchでは、JavaScriptでのプログラミングに関するこの問題解決に非常に力を入れてきました。バックグラウンドで非同期のコマンドキューを実装したので、テスターは心配する必要はありません。

Nightwatchには内部コマンドキューがあるため、JavaScriptの非同期の問題を心配する必要はありません。

しかし、まれにNightwatch APIから値を取り出してプレーンなJavaScriptで使用する場合、asyncawaitが不可欠になります。テスト中はこれは非常にまれですが、そのような状況に遭遇した場合は、すべてのNightwatch APIがPromiseを返すため、awaitを使用して値を取得できます。

it('Use values from the webpage', async function(browser) {
  const href = await browser.element.find('.navigation-list li a').getAttribute('href').value
  // You can do anything with the href value here after
  console.log(href)
  MyAPI.track(href)
})

🗂 マルチタブ操作

ブラウジング中に一般的に遭遇するシナリオの1つは、新しいタブで開かれるリンクをクリックすることです。Nightwatchは、開いているさまざまなドキュメント間を切り替えるAPIを提供することで、このシナリオをテストできるようにします。

GitHubアイコンをクリックし、開かれた新しいタブのURLを確認します。

it('Should lead to the GitHub repo on clicking the Github icon', async function (browser) {
  browser.element.find('ul.navigation-list.social li:nth-child(2) a').click()
  // wait until window handle for the new window is available
  browser.waitUntil(async function () {
    const windowHandles = await browser.window.getAllHandles()
    return windowHandles.length === 2
  })

  const allWindows = await browser.window.getAllHandles()
  browser.window.switchTo(allWindows[1])

  browser.assert.urlContains('github.com/nightwatchjs')
})

ブラウザでは、すべてのタブはウィンドウと見なされます。結局のところ、それぞれに異なるwindowオブジェクトがあります。ここでは、新しいタブが利用可能になるのを待ち、新しいタブに切り替え、ブラウザのURLがGitHubリポジトリのURLと一致するかどうかを確認します。

💡
.waitUntil(<condition>) は、条件が「truthy」値になるまで待機し、そうでない場合は失敗します。

<condition>は、値を返す関数または待機するPromiseのいずれかになります。

.window.getAllHandles() は、すべてのウィンドウのID配列を返します。

.window.switchTo(<id>) はウィンドウIDを受け取り、テストランナーをそのウィンドウに切り替えます。

🌍 ジオロケーションのエミュレート

WebサイトまたはWebアプリがアクセス元の場所に基づいて変更される場合、これらのすべての場所についてWebサイトをテストすることが重要になります。Chrome DevTools Protocolの導入により、Nightwatchは1つのコマンドだけでテスト実行中にブラウザのジオロケーションをモックすることをサポートしています。

地球上の3つの異なる場所からアドレスを確認します。

it('sets and verifies the geolocation to Japan, USA and Denmark', function (browser) {
  const location_tests = [
    {
      location: { latitude: 35.689487, longitude: 139.691706, accuracy: 100 },
      // Tokyo Metropolitan Government Office, 都庁通り, Nishi - Shinjuku 2 - chome, Shinjuku, 163 - 8001, Japan
      test_text: 'Japan',
    },
    {
      location: { latitude: 40.730610, longitude: -73.935242, accuracy: 100 },
      // 38-20 Review Avenue, New York, NY 11101, United States of America
      test_text: 'New York',
    },
    {
      location: { latitude: 55.676098, longitude: 12.568337, accuracy: 100 },
      // unnamed road, 1550 København V, Denmark
      test_text: 'Denmark',
    }
  ]

  const waitTillLoad = async function () {
    const geo_dom_class = await browser.element.find('#geolocation_address')
      .getAttribute('class').value
    return !geo_dom_class.includes('text-muted')
  }

  location_tests.forEach(obj => {
    browser.setGeolocation(obj.location).navigateTo('https://www.where-am-i.co/')
    browser.waitUntil(waitTillLoad)
    browser.element.find('#geolocation_address').getText().assert.contains(obj.test_text)
  })
})

説明

location_tests配列の各場所を順番に処理し、ブラウザをそのジオロケーションに設定します。text-mutedクラスが消えたかどうかを確認することで、.waitUntil(fn)を使用してアドレスが読み込まれるのを待ちます。次に、アドレスに正しいテキストがあるかどうかを確認します。

💡
.setGeolocation({ latitude, longitude, accuracy }) は、ジオロケーションをモックしながら、ブラウザのジオロケーションを指定されたlatitudelongitudeaccuracyに設定します。

テストを実行するには、最初にwww.where-am-i.coにアクセスし、Webサイトがあなたの場所へのアクセスを許可する必要があります。

心配しないでください。テストが終了したら、いつでも場所のアクセス許可をリセットできます。

このコマンドは、Google ChromeやMicrosoft EdgeなどのChromiumベースのブラウザでのみ機能します。

次回

ページオブジェクトモデルを用いたスケーラブルなテストの記述

今日は、Nightwatchを使ってウェブサイトやウェブアプリの詳細なテストを行い、複雑なユースケースやシナリオをシミュレートする方法について多くを学びました。ウェブテストに関するシリーズの次の章では、テスト記述パターンを探求し、ページオブジェクトモデル(POM)を紹介します。テストコードの構造と保守性を向上させる様々なパターンを説明し、テストスクリプトにおける再利用性とモジュール性を促進するデザインパターンであるPOMの実装方法を学びます。

コミュニティに参加しましょう 💬

ご質問がございましたら、Discordサーバーにアクセスしてご連絡ください。当社のコミュニティは常にサポートを提供し、洞察を共有し、テスト関連のあらゆるお問い合わせに対応いたします。皆様の積極的な参加を歓迎しており、Discordコミュニティで皆様とつながることを楽しみにしています。Twitterからもご連絡いただけます。

楽しいテストを! 🎉