Passer au contenu principal

Mocking

Lors de l'écriture de tests, ce n'est qu'une question de temps avant de devoir créer une "fausse" version d'un service interne ou externe. C'est ce qu'on appelle communément la moquerie. WebdriverIO fournit des fonctions utilitaires pour vous aider. Vous pouvez importer { fn, spyOn, mock, unmock } depuis '@wdio/browser-runner' pour y accéder. Voir plus d'informations sur les utilitaires de simulation disponibles dans la documentation de l'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 factices que vous pouvez utiliser pour tester si ces fonctions ont Ă©tĂ© appelĂ©es. Vous pouvez importer ces mĂ©thodes via :

import { fn, spy } 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éé.

L'exemple complet peut ĂȘtre trouvĂ© dans Component Testing Example dĂ©pĂŽt.

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 rĂ©exporte simplement @vitest/spy ici, qui est une implĂ©mentation d'espionnage lĂ©gĂšre compatible avec Jest qui peut ĂȘtre utilisĂ©e avec WebdriverIOs attend matchers. Vous pouvez trouver plus de documentation sur ces fonctions fictives sur la page du projet Vitest.

Bien sûr, vous pouvez également installer et importer tout autre framework d'espionnage, par exemple SinonJS, tant qu'il prend en charge l'environnement du navigateur.

Modules​

Simuler des modules locaux ou observer 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Ă©clarer son implĂ©mentation.

Il existe deux façons de simuler des fonctions : soit en crĂ©ant une fonction fictive Ă  utiliser dans le code de test, soit en Ă©crivant un simulacre manuel pour remplacer une dĂ©pendance de module.

Masquage des imports de fichiers​

Imaginons que notre composant importe une méthode utilitaire à partir 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)
})
})

DĂ©pendances de masquage​

Supposons que nous ayons une classe qui récupÚre les utilisateurs de notre API. La classe utilise axios pour appeler l'API puis retourne 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, afin de tester cette méthode sans toucher à l'API (et donc en créant des tests lents et fragiles), nous pouvons utiliser la fonction mock(...) pour simuler automatiquement le module axios.

Une fois que nous avons simulé le module, nous pouvons fournir un mockResolvedValue for .get qui renvoie les données sur lesquelles nous voulons que notre test s'appuie. En effet, nous disons que nous voulons que axios.get('/users.json') renvoie une fausse réponse.

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 son implĂ©mentation rĂ©elle :

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

The original module will be passed into the mock factory which you can use to e.g. partially mock a dependency:

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

Masques manuelles​

Les mocks manuels sont dĂ©finis en Ă©crivant un module dans un sous-rĂ©pertoire __mocks__/ (voir aussi l'option automockDir). Si le module que vous utilisez est un module Node (par ex. lodash), le bouchon doit ĂȘtre placĂ© dans le rĂ©pertoire __mocks__ et sera automatiquement bouchĂ©. Il n'est pas nĂ©cessaire d'appeler explicitement mock('module_name').

Les modules de portĂ©e (Ă©galement appelĂ©s packages de portĂ©e) peuvent ĂȘtre simulĂ©s en crĂ©ant un fichier dans une structure de rĂ©pertoires qui correspond au nom du module de portĂ©e. Par exemple, pour simuler un module Ă©tendu 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'un mock manuel existe pour un module donnĂ©, WebdriverIO utilisera ce module lors de l'appel explicite de mock('moduleName'). Cependant, lorsque automock est dĂ©fini Ă  true, l'implĂ©mentation manuelle du bouchon sera utilisĂ©e Ă  la place du bouchon automatiquement crĂ©Ă©, mĂȘme si mock('moduleName') n'est pas appelĂ©. Pour dĂ©sactiver ce comportement, vous devrez appeler explicitement unmock('moduleName') dans les tests qui doivent utiliser l'implĂ©mentation rĂ©elle du module, par exemple :

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

unmock('lodash')

Hoisting​

Afin de faire fonctionner le bouchon dans le navigateur, WebdriverIO réécrit les fichiers de test et élÚve les appels de bouchon au-dessus de tout le reste (voir aussi ce post de blog sur le problÚme de levage dans Jest). Cela limite la façon dont vous pouvez passer des variables dans le résolveur de bouchon, 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 dans le 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 des requĂȘtes de navigateur simulĂ©es, par exemple des appels d'API, rendez-vous Ă  la section Request Mock and Spies.

Welcome! How can I help?

WebdriverIO AI Copilot