跳至主要内容

模拟

在编写测试时,您迟早需要创建内部或外部服务的“伪造”版本。这通常称为模拟。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'
}))
})
})

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.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 获取用户。该类使用axios调用 API,然后返回包含所有用户的data属性

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 调用),请转到请求模拟和间谍部分。

欢迎!我如何提供帮助?

WebdriverIO AI Copilot