Simulation (Mocking)
Lors de l'écriture de tests, ce n'est qu'une question de temps avant que vous ayez besoin de créer une version "factice" d'un service interne ou externe. C'est ce qu'on appelle communément le mocking (simulation). WebdriverIO fournit des fonctions utilitaires pour vous aider. Vous pouvez import { fn, spyOn, mock, unmock } from '@wdio/browser-runner' pour y accéder. Consultez plus d'informations sur les utilitaires de simulation disponibles dans la documentation API.
Fonctionsâ
Afin de valider si certains gestionnaires de fonctions sont appelés dans le cadre de vos tests de composants, le module @wdio/browser-runner exporte des primitives de simulation que vous pouvez utiliser pour tester si ces fonctions ont été appelées. Vous pouvez importer ces méthodes via :
import { fn, spyOn } from '@wdio/browser-runner'
En important fn, vous pouvez créer une fonction espion (mock) pour suivre son exécution et avec spyOn suivre une méthode sur un objet déjà créé.
- Mocks
- Spies
L'exemple complet peut ĂȘtre trouvĂ© dans le dĂ©pĂŽt 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'
}))
})
})
L'exemple complet peut ĂȘtre trouvĂ© dans le rĂ©pertoire 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 rĂ©exporte simplement @vitest/spy ici, qui est une implĂ©mentation lĂ©gĂšre d'espion compatible avec Jest qui peut ĂȘtre utilisĂ©e avec les matchers expect de WebdriverIO. Vous pouvez trouver plus de documentation sur ces fonctions de simulation sur la page du projet Vitest.
Bien sûr, vous pouvez également installer et importer n'importe quel autre framework d'espionnage, par exemple SinonJS, tant qu'il prend en charge l'environnement du navigateur.
Modulesâ
Simulez des modules locaux ou observez des bibliothĂšques tierces, qui sont invoquĂ©es dans un autre code, vous permettant de tester les arguments, la sortie ou mĂȘme de redĂ©finir leur implĂ©mentation.
Il existe deux façons de simuler des fonctions : soit en créant une fonction de simulation à utiliser dans le code de test, soit en écrivant une simulation manuelle pour remplacer une dépendance de module.
Simulation des importations de fichiersâ
Imaginons que notre composant importe une méthode utilitaire d'un fichier pour gérer un clic.
export function handleClick () {
// handler implementation
}
Dans notre composant, le gestionnaire de clic est utilisé comme suit :
import { handleClick } from './utils.js'
@customElement('simple-button')
export class SimpleButton extends LitElement {
render() {
return html`<button @click="${handleClick}">Click me!</button>`
}
}
Pour simuler le handleClick de utils.js, nous pouvons utiliser la méthode mock dans notre test comme suit :
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)
})
})
Simulation des dĂ©pendancesâ
Supposons que nous ayons une classe qui récupÚre des utilisateurs depuis notre API. La classe utilise axios pour appeler l'API puis renvoie l'attribut data qui contient tous les utilisateurs :
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data)
}
}
export default Users
Maintenant, pour tester cette méthode sans réellement accéder à l'API (et ainsi créer des tests lents et fragiles), nous pouvons utiliser la fonction mock(...) pour simuler automatiquement le module axios.
Une fois que nous simulons le module, nous pouvons fournir un mockResolvedValue pour .get qui renvoie les données sur lesquelles notre test doit s'appuyer. En effet, nous disons que nous voulons que axios.get('/users.json') renvoie une réponse factice.
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)
})
})
Partielsâ
Des sous-ensembles d'un module peuvent ĂȘtre simulĂ©s et le reste du module peut conserver leur implĂ©mentation rĂ©elle :
export const foo = 'foo';
export const bar = () => 'bar';
export default () => 'baz';
Le module original sera passé dans la fabrique de simulation que vous pouvez utiliser pour, par exemple, simuler partiellement une dépendance :
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');
})
})
Simulations manuellesâ
Les simulations manuelles sont dĂ©finies en Ă©crivant un module dans un sous-rĂ©pertoire __mocks__/ (voir aussi l'option automockDir). Si le module que vous simulez est un module Node (par exemple : lodash), la simulation doit ĂȘtre placĂ©e dans le rĂ©pertoire __mocks__ et sera automatiquement simulĂ©e. Il n'est pas nĂ©cessaire d'appeler explicitement mock('module_name').
Les modules Ă portĂ©e (Ă©galement connus sous le nom de packages Ă portĂ©e) peuvent ĂȘtre simulĂ©s en crĂ©ant un fichier dans une structure de rĂ©pertoire qui correspond au nom du module Ă portĂ©e. Par exemple, pour simuler un module Ă portĂ©e appelĂ© @scope/project-name, crĂ©ez un fichier Ă __mocks__/@scope/project-name.js, en crĂ©ant le rĂ©pertoire @scope/ en consĂ©quence.
.
âââ config
âââ __mocks__
â âââ axios.js
â âââ lodash.js
â âââ @scope
â âââ project-name.js
âââ node_modules
âââ views
Lorsqu'une simulation manuelle existe pour un module donnĂ©, WebdriverIO utilisera ce module lors de l'appel explicite Ă mock('moduleName'). Cependant, lorsque automock est dĂ©fini sur true, l'implĂ©mentation de la simulation manuelle sera utilisĂ©e Ă la place de la simulation créée automatiquement, mĂȘme si mock('moduleName') n'est pas appelĂ©. Pour dĂ©sactiver ce comportement, vous devrez appeler explicitement unmock('moduleName') dans les tests qui devraient utiliser l'implĂ©mentation rĂ©elle du module, par exemple :
import { unmock } from '@wdio/browser-runner'
unmock('lodash')
Hissage (Hoisting)â
Pour que la simulation fonctionne dans le navigateur, WebdriverIO réécrit les fichiers de test et hisse les appels de simulation au-dessus de tout le reste (voir aussi cet article de blog sur le problÚme de hissage dans Jest). Cela limite la façon dont vous pouvez passer des variables dans le résolveur de simulation, par exemple :
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
}))
Pour résoudre ce problÚme, vous devez définir toutes les variables utilisées à l'intérieur du résolveur, par exemple :
/**
* âïž 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
}
})
RequĂȘtesâ
Si vous recherchez Ă simuler des requĂȘtes de navigateur, par exemple des appels API, consultez la section Simulation et espionnage de requĂȘtes.