モック
テストを作成する際、内部サービスや外部サービスの「偽」バージョンを作成する必要が生じるのは時間の問題です。これは一般的にモックと呼ばれます。WebdriverIOは、これを支援するユーティリティ関数を提供しています。import { fn, spyOn, mock, unmock } from '@wdio/browser-runner' を使用してアクセスできます。利用可能なモックユーティリティの詳細については、APIドキュメントを参照してください。
関数
コンポーネントテストの一部として特定の関数ハンドラーが呼び出されているかどうかを検証するには、@wdio/browser-runnerモジュールが、これらの関数が呼び出されたかどうかをテストするために使用できるモックプリミティブをエクスポートします。これらのメソッドは以下のようにインポートできます。
import { fn, spyOn } from '@wdio/browser-runner'
fnをインポートすることで、実行を追跡するためのスパイ関数(モック)を作成でき、spyOnを使用すると、すでに作成されたオブジェクトのメソッドを追跡できます。
- モック
- スパイ
完全な例は、コンポーネントテストの例リポジトリにあります。
import React from 'react'
import { $, expect } from '@wdio/globals'
import { fn } from '@wdio/browser-runner'
import { Key } from 'webdriverio'
import { render } from '@testing-library/react'
import LoginForm from '../components/LoginForm'
describe('LoginForm', () => {
    it('should call onLogin handler if username and password was provided', async () => {
        const onLogin = fn()
        render(<LoginForm onLogin={onLogin} />)
        await $('input[name="username"]').setValue('testuser123')
        await $('input[name="password"]').setValue('s3cret')
        await browser.keys(Key.Enter)
        /**
         * verify the handler was called
         */
        expect(onLogin).toBeCalledTimes(1)
        expect(onLogin).toBeCalledWith(expect.equal({
            username: 'testuser123',
            password: 's3cret'
        }))
    })
})
完全な例は、examplesディレクトリにあります。
import { expect, $ } from '@wdio/globals'
import { spyOn } from '@wdio/browser-runner'
import { html, render } from 'lit'
import { SimpleGreeting } from './components/LitComponent.ts'
const getQuestionFn = spyOn(SimpleGreeting.prototype, 'getQuestion')
describe('Lit Component testing', () => {
    it('should render component', async () => {
        render(
            html`<simple-greeting name="WebdriverIO" />`,
            document.body
        )
        const innerElem = await $('simple-greeting').$('p')
        expect(await innerElem.getText()).toBe('Hello, WebdriverIO! How are you today?')
    })
    it('should render with mocked component function', async () => {
        getQuestionFn.mockReturnValue('Does this work?')
        render(
            html`<simple-greeting name="WebdriverIO" />`,
            document.body
        )
        const innerElem = await $('simple-greeting').$('p')
        expect(await innerElem.getText()).toBe('Hello, WebdriverIO! Does this work?')
    })
})
WebdriverIOは、ここでは@vitest/spyを再エクスポートします。これは、WebdriverIOのexpectマッチャーで使用できる軽量のJest互換スパイ実装です。これらのモック関数の詳細については、Vitestプロジェクトページをご覧ください。
もちろん、ブラウザ環境をサポートしている限り、SinonJSなどの他のスパイフレームワークをインストールしてインポートすることもできます。
モジュール
ローカルモジュールをモックするか、他のコードで呼び出されるサードパーティライブラリを監視し、引数、出力、またはその実装を再宣言してテストできるようにします。
関数をモックするには2つの方法があります。テストコードで使用するモック関数を作成する方法と、モジュール依存関係を上書きする手動モックを作成する方法です。
ファイルインポートのモック
コンポーネントがクリックを処理するためにファイルからユーティリティメソッドをインポートしているとしましょう。
export function handleClick () {
    // handler implementation
}
コンポーネントでは、クリックハンドラーは次のように使用されます。
import { handleClick } from './utils.js'
@customElement('simple-button')
export class SimpleButton extends LitElement {
    render() {
        return html`<button @click="${handleClick}">Click me!</button>`
    }
}
utils.jsのhandleClickをモックするには、次のようにテストでmockメソッドを使用できます。
import { expect, $ } from '@wdio/globals'
import { mock, fn } from '@wdio/browser-runner'
import { html, render } from 'lit'
import { SimpleButton } from './LitComponent.ts'
import { handleClick } from './utils.js'
/**
 * mock named export "handleClick" of `utils.ts` file
 */
mock('./utils.ts', () => ({
    handleClick: fn()
}))
describe('Simple Button Component Test', () => {
    it('call click handler', async () => {
        render(html`<simple-button />`, document.body)
        await $('simple-button').$('button').click()
        expect(handleClick).toHaveBeenCalledTimes(1)
    })
})
依存関係のモック
APIからユーザーをフェッチするクラスがあるとします。このクラスは、APIを呼び出すためにaxiosを使用し、すべてのユーザーを含むデータ属性を返します。
import axios from 'axios';
class Users {
  static all() {
    return axios.get('/users.json').then(resp => resp.data)
  }
}
export default Users
ここで、実際にAPIをヒットせずに(したがって、遅くて脆弱なテストを作成せずに)このメソッドをテストするために、mock(...)関数を使用して、axiosモジュールを自動的にモックできます。
モジュールをモックしたら、テストでアサートしたいデータを返す.getに対してmockResolvedValueを提供できます。事実上、axios.get('/users.json')が偽の応答を返すようにしたいと言っていることになります。
import axios from 'axios'; // imports defined mock
import { mock, fn } from '@wdio/browser-runner'
import Users from './users.js'
/**
 * mock default export of `axios` dependency
 */
mock('axios', () => ({
    default: {
        get: fn()
    }
}))
describe('User API', () => {
    it('should fetch users', async () => {
        const users = [{name: 'Bob'}]
        const resp = {data: users}
        axios.get.mockResolvedValue(resp)
        // or you could use the following depending on your use case:
        // axios.get.mockImplementation(() => Promise.resolve(resp))
        const data = await Users.all()
        expect(data).toEqual(users)
    })
})
パーシャル
モジュールのサブセットをモックし、モジュールの残りの部分は実際の実装を維持できます。
export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';
元のモジュールはモックファクトリに渡され、それを使用してたとえば依存関係を部分的にモックすることができます。
import { mock, fn } from '@wdio/browser-runner'
import defaultExport, { bar, foo } from './foo-bar-baz.js';
mock('./foo-bar-baz.js', async (originalModule) => {
    // Mock the default export and named export 'foo'
    // and propagate named export from the original module
    return {
        __esModule: true,
        ...originalModule,
        default: fn(() => 'mocked baz'),
        foo: 'mocked foo',
    }
})
describe('partial mock', () => {
    it('should do a partial mock', () => {
        const defaultExportResult = defaultExport();
        expect(defaultExportResult).toBe('mocked baz');
        expect(defaultExport).toHaveBeenCalled();
        expect(foo).toBe('mocked foo');
        expect(bar()).toBe('bar');
    })
})
手動モック
手動モックは、__mocks__/(automockDirオプションも参照)サブディレクトリにモジュールを記述することで定義されます。モックしているモジュールがNodeモジュール(例:lodash)の場合、モックは__mocks__ディレクトリに配置する必要があり、自動的にモックされます。mock('module_name')を明示的に呼び出す必要はありません。
スコープ付きモジュール(スコープ付きパッケージとも呼ばれます)は、スコープ付きモジュールの名前に一致するディレクトリ構造にファイルを作成することでモックできます。たとえば、@scope/project-nameという名前のスコープ付きモジュールをモックするには、__mocks__/@scope/project-name.jsにファイルを作成し、それに応じて@scope/ディレクトリを作成します。
.
├── config
├── __mocks__
│   ├── axios.js
│   ├── lodash.js
│   └── @scope
│       └── project-name.js
├── node_modules
└── views
特定のモジュールに対して手動モックが存在する場合、WebdriverIOはmock('moduleName')を明示的に呼び出すときにそのモジュールを使用します。ただし、automockがtrueに設定されている場合、mock('moduleName')が呼び出されなくても、自動的に作成されたモックの代わりに手動モックの実装が使用されます。この動作をオプトアウトするには、実際のモジュール実装を使用する必要があるテストで、unmock('moduleName')を明示的に呼び出す必要があります。例:
import { unmock } from '@wdio/browser-runner'
unmock('lodash')
ホイスティング
ブラウザでモックを機能させるために、WebdriverIOはテストファイルを書き換え、モック呼び出しを他のすべてのものの上にホイストします(Jestでのホイスティングの問題に関するこのブログ投稿も参照してください)。これにより、モックリゾルバーに変数を渡す方法が制限されます。例:
import dep from 'dependency'
const variable = 'foobar'
/**
 * ❌ this fails as `dep` and `variable` are not defined inside the mock resolver
 */
mock('./some/module.ts', () => ({
    exportA: dep,
    exportB: variable
}))
これを修正するには、リゾルバー内で使用するすべての変数を定義する必要があります。例:
/**
 * ✔️ this works as all variables are defined within the resolver
 */
mock('./some/module.ts', async () => {
    const dep = await import('dependency')
    const variable = 'foobar'
    return {
        exportA: dep,
        exportB: variable
    }
})
リクエスト
ブラウザのリクエスト(API呼び出しなど)のモックをお探しの場合は、リクエストモックとスパイのセクションにアクセスしてください。