Hoppa till huvudinnehåll

Mockning

När du skriver tester är det bara en tidsfråga innan du behöver skapa en "falsk" version av en intern - eller extern - tjänst. Detta kallas vanligtvis för mockning. WebdriverIO tillhandahåller hjälpfunktioner för detta. Du kan använda import { fn, spyOn, mock, unmock } from '@wdio/browser-runner' för att få tillgång till dem. Se mer information om tillgängliga mockningsverktyg i API-dokumentationen.

Funktioner

För att validera om vissa funktionshanterare anropas som en del av dina komponenttester, exporterar modulen @wdio/browser-runner mockningsprimitiver som du kan använda för att testa om dessa funktioner har anropats. Du kan importera dessa metoder via:

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

Genom att importera fn kan du skapa en spejfunktion (mock) för att spåra dess exekvering och med spyOn spåra en metod på ett redan skapat objekt.

Det fullständiga exemplet finns i arkivet Component Testing Example.

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 återexporterar bara @vitest/spy här, vilket är en lättviktig Jest-kompatibel spionimplementering som kan användas med WebdriverIOs expect-matchare. Du kan hitta mer dokumentation om dessa mockfunktioner på Vitest-projektets sida.

Självklart kan du också installera och importera andra spionramverk, t.ex. SinonJS, så länge de stöder webbläsarmiljön.

Moduler

Mocka lokala moduler eller observera tredjepartsbibliotek som anropas i annan kod, vilket låter dig testa argument, utdata eller till och med omdeklarera implementationen.

Det finns två sätt att mocka funktioner: Antingen genom att skapa en mockfunktion för användning i testkod, eller genom att skriva en manuell mock för att åsidosätta en modulberoende.

Mockning av filimporter

Låt oss föreställa oss att vår komponent importerar en hjälpmetod från en fil för att hantera ett klick.

export function handleClick () {
// handler implementation
}

I vår komponent används klickhanteraren på följande sätt:

import { handleClick } from './utils.js'

@customElement('simple-button')
export class SimpleButton extends LitElement {
render() {
return html`<button @click="${handleClick}">Click me!</button>`
}
}

För att mocka handleClick från utils.js kan vi använda mock-metoden i vårt test på följande sätt:

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

Mockning av beroenden

Anta att vi har en klass som hämtar användare från vår API. Klassen använder axios för att anropa API:et och returnerar sedan dataattributet som innehåller alla användare:

import axios from 'axios';

class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data)
}
}

export default Users

För att testa denna metod utan att faktiskt träffa API:et (och därmed skapa långsamma och ömtåliga tester), kan vi använda funktionen mock(...) för att automatiskt mocka axios-modulen.

När vi mockar modulen kan vi tillhandahålla ett mockResolvedValue för .get som returnerar de data vi vill att vårt test ska hävda mot. I praktiken säger vi att vi vill att axios.get('/users.json') ska returnera ett fejkat svar.

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

Partiell mockning

Delmängder av en modul kan mockas medan resten av modulen behåller sin faktiska implementering:

export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';

Den ursprungliga modulen kommer att skickas till mockfabriken som du kan använda för att t.ex. delvis mocka ett beroende:

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

Manuella mockar

Manuella mockar definieras genom att skriva en modul i en undermapp __mocks__/ (se även alternativet automockDir). Om modulen du mockar är en Node-modul (t.ex.: lodash), bör mocken placeras i katalogen __mocks__ och kommer att automatiskt mockas. Det finns ingen anledning att uttryckligen anropa mock('module_name').

Scoped modules (även kallade scoped packages) kan mockas genom att skapa en fil i en katalogstruktur som matchar namnet på den scoped module. För att till exempel mocka en scoped module kallad @scope/project-name, skapa en fil på __mocks__/@scope/project-name.js, genom att skapa katalogen @scope/ på lämpligt sätt.

.
├── config
├── __mocks__
│ ├── axios.js
│ ├── lodash.js
│ └── @scope
│ └── project-name.js
├── node_modules
└── views

När en manuell mock finns för en given modul kommer WebdriverIO att använda den modulen när mock('moduleName') uttryckligen anropas. Men när automock är satt till true kommer den manuella mockimplementationen att användas istället för den automatiskt skapade mocken, även om mock('moduleName') inte anropas. För att avanmäla sig från detta beteende måste du uttryckligen anropa unmock('moduleName') i tester som ska använda den faktiska modulimplementationen, t.ex.:

import { unmock } from '@wdio/browser-runner'

unmock('lodash')

Hoisting

För att få mockning att fungera i webbläsaren, skriver WebdriverIO om testfilerna och lyfter upp mockanropen över allt annat (se även detta blogginlägg om lyftproblemet i Jest). Detta begränsar hur du kan skicka in variabler i mock-resolvern, t.ex.:

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

För att fixa detta måste du definiera alla använda variabler inuti resolvern, t.ex.:

/**
* ✔️ 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
}
})

Förfrågningar

Om du letar efter att mocka webbläsarförfrågningar, t.ex. API-anrop, gå till avsnittet Request Mock and Spies.

Welcome! How can I help?

WebdriverIO AI Copilot