跳至主要內容

模擬 (Mocking)

當編寫測試時,您很快就會需要建立一個內部或外部服務的「假」版本。這通常被稱為模擬 (mocking)。WebdriverIO 提供了實用程式函數來幫助您。您可以 import { fn, spyOn, mock, unmock } from '@wdio/browser-runner' 來存取它。請參閱API 文件中關於可用模擬工具的更多資訊。

函數

為了驗證某些函式處理程序是否在您的元件測試中被呼叫,@wdio/browser-runner 模組匯出了您可以用來測試這些函式是否被呼叫的模擬原語 (mocking primitives)。您可以透過以下方式匯入這些方法:

import { fn, spyOn } from '@wdio/browser-runner'

透過匯入 fn,您可以建立一個 spy 函式(模擬)來追蹤其執行情況,並使用 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'
}))
})
})

WebdriverIO 只是在此重新匯出@vitest/spy,它是一個輕量級的 Jest 相容間諜實作,可以與 WebdriverIO 的expect 比對器一起使用。您可以在Vitest 專案頁面上找到有關這些模擬函數的更多文件。

當然,您也可以安裝和匯入任何其他間諜框架,例如SinonJS,只要它支援瀏覽器環境即可。

模組

模擬本地模組或觀察在某些其他程式碼中調用的第三方程式庫,讓您可以測試參數、輸出,甚至重新宣告其實作。

有兩種方法可以模擬函數:一種是建立一個模擬函數在測試程式碼中使用,另一種是編寫一個手動模擬來覆寫模組相依性。

模擬檔案匯入

假設我們的元件正在從檔案匯入一個實用程式方法來處理點擊事件。

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.jshandleClick,我們可以在測試中使用 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 獲取使用者。該類別使用axios來呼叫 API,然後傳回包含所有使用者的資料屬性。

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')

提升 (Hoisting)

為了使模擬在瀏覽器中正常運作,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 呼叫),請前往請求模擬和間諜章節。

歡迎!我能如何協助您?

WebdriverIO AI Copilot