Tests de régression visuelle avec Chromatic, Storybook et Mock Service Workers
Cet article vous expliquera comment configurer Chromatic et établir des tests de régression visuelle dans votre application. Voici le code final basé sur GitHub.
- Chromatique est une chaîne d’outils basée sur le cloud pour Storybook qui permet la publication de Storybook, les tests d’interface utilisateur et la révision de l’interface utilisateur. Nous allons automatiser les tests de régression visuelle avec Chromatic et GitHub Actions.
- MSW est une bibliothèque qui se moque des requêtes API en les interceptant au niveau du réseau. MSW permet à votre application de fonctionner sans se moquer d’un code spécifique dans votre base de code. Nous utiliserons le serveur MSW pour Storybook et les tests unitaires.
Voici les dépendances à récupérer :
- suivant@13.0.6
- réagir@18.2.0
- apollon/client@3.7.2
- graphql-codegen/cli@2.16.1
- msw@0.49.2
- livre d’histoires @ v6
Supposons que nous ayons une application créée par Next.js et le serveur API GraphQL. Pour commencer rapidement, j’ai configuré l’application qui a installé GraphQL, apollo-clientet graphql-codegen. Vous pouvez voir les détails dans ce RP.
Notre application effectue une requête API sur le pays.revorblades.com serveur et obtient une liste de pays comme ça. Nous allons implémenter un test de régression visuelle sur cette page.
Voici un aperçu de la mise en œuvre.
- Configurer MSW
- Installer le livre de contes
- Installer le plugin storybook-addon-next-router
- Installer le plugin msw-storybook-addon
- Créer des histoires
- Configurer Chromatique
- Automatisez la vérification de l’interface utilisateur avec les actions GitHub
- Tests unitaires
Commençons par installer le msw
package dans notre projet.
yarn add -D msw
Nous allons également installer le deepmerge
et le utility-types
package pour étendre les données fictives de manière flexible.
yarn add -D deepmerge utility-types
Pour conserver les modules liés à l’API mocking dans un seul répertoire, nous allons créer src/mocks
répertoire à l’aide de la commande suivante :
mkdir src/mocks
Pour définir un mock pour les requêtes GraphQL, nous allons créer src/mocks/queries
annuaire:
mkdir src/mocks/queries
Dans notre application, nous avons le Countries
requête comme ceci:
query Countries($filer: CountryFilterInput) {
countries(filter: $filer) {
name
native
capital
emoji
currency
languages {
code
name
}
}
}
Pour gérer la requête GraphQL, nous avons besoin d’un résolveur pour renvoyer une réponse simulée et créer des données simulées. Pour regrouper les modules dans un seul dossier, nous allons structurer les fichiers comme suit :
src/mocks/
└── queries
├── countries
│ ├── countriesQuery.ts
│ ├── data.ts
│ ├── index.ts
│ └── type.ts
└── handlers.ts
Créer src/mocks/queries/countries/countriesQuery.ts
:
import { graphql } from 'msw'
import { data } from './data'
import { Options, Query } from './type'export const countriesQuery = (options?: Options) => {
return graphql.query<Query>('Countries', (_, res, ctx) => {
if (options?.networkError) {
return res(
ctx.errors([
{
message: 'Network request failed',
graphQLErrors: [],
networkError: new Error('error'),
errorMessage: 'error',
extraInfo: {},
},
]),
)
}
return res(ctx.data(data(options?.res, options?.deepMergeOptions)))
})
}
La countriesQuery
renvoie un résolveur de réponse contre le Countries
requête qui renvoie des données simulées via la res
fonction.
Créer src/mocks/queries/countries/type.ts
:
import deepmerge from 'deepmerge'
import { CountriesQuery as Query } from 'src/graphql/types/index.mock'
import { DeepPartial } from 'utility-types'export type Response = DeepPartial<Query>
export type { CountriesQuery as Query } from 'src/graphql/types/index.mock'
export type Options = {
res?: Response
networkError?: boolean
deepMergeOptions?: deepmerge.Options
}
Et créer src/mocks/queries/countries/data.ts
:
import deepmerge from 'deepmerge'
import { Response, Query } from './type'export const data = (
options?: Response,
deepMergeOptions?: deepmerge.Options,
): Query =>
deepmerge<Query>(
{
__typename: 'Query',
countries: [
{
name: 'Andorra',
native: 'Andorra',
capital: 'Andorra la Vella',
emoji: '🇦🇩',
currency: 'EUR',
languages: [{ code: 'ca', name: 'Catalan', __typename: 'Language' }],
__typename: 'Country',
},
{
name: 'United Arab Emirates',
native: 'دولة الإمارات العربية المتحدة',
capital: 'Abu Dhabi',
emoji: '🇦🇪',
currency: 'AED',
languages: [{ code: 'ar', name: 'Arabic', __typename: 'Language' }],
__typename: 'Country',
},
{
name: 'Afghanistan',
native: 'افغانستان',
capital: 'Kabul',
emoji: '🇦🇫',
currency: 'AFN',
languages: [
{ code: 'ps', name: 'Pashto', __typename: 'Language' },
{ code: 'uz', name: 'Uzbek', __typename: 'Language' },
{ code: 'tk', name: 'Turkmen', __typename: 'Language' },
],
__typename: 'Country',
},
{
name: 'Antigua and Barbuda',
native: 'Antigua and Barbuda',
capital: "Saint John's",
emoji: '🇦🇬',
currency: 'XCD',
languages: [{ code: 'en', name: 'English', __typename: 'Language' }],
__typename: 'Country',
},
{
name: 'Anguilla',
native: 'Anguilla',
capital: 'The Valley',
emoji: '🇦🇮',
currency: 'XCD',
languages: [{ code: 'en', name: 'English', __typename: 'Language' }],
__typename: 'Country',
},
{
name: 'Albania',
native: 'Shqipëria',
capital: 'Tirana',
emoji: '🇦🇱',
currency: 'ALL',
languages: [{ code: 'sq', name: 'Albanian', __typename: 'Language' }],
__typename: 'Country',
},
{
name: 'Armenia',
native: 'Հայաստան',
capital: 'Yerevan',
emoji: '🇦🇲',
currency: 'AMD',
languages: [
{ code: 'hy', name: 'Armenian', __typename: 'Language' },
{ code: 'ru', name: 'Russian', __typename: 'Language' },
],
__typename: 'Country',
},
{
name: 'Angola',
native: 'Angola',
capital: 'Luanda',
emoji: '🇦🇴',
currency: 'AOA',
languages: [
{ code: 'pt', name: 'Portuguese', __typename: 'Language' },
],
__typename: 'Country',
},
{
name: 'Antarctica',
native: 'Antarctica',
capital: null,
emoji: '🇦🇶',
currency: null,
languages: [],
__typename: 'Country',
},
{
name: 'Argentina',
native: 'Argentina',
capital: 'Buenos Aires',
emoji: '🇦🇷',
currency: 'ARS',
languages: [
{ code: 'es', name: 'Spanish', __typename: 'Language' },
{ code: 'gn', name: 'Guarani', __typename: 'Language' },
],
__typename: 'Country',
},
],
},
(options || {}) as Query,
{
arrayMerge(target: any[], source: any[]): any[] {
if (!source.length) return source
return [...target, ...source]
},
...deepMergeOptions,
},
)
Ce sont les données simulées auxquelles le serveur MSW répondra contre le Countries
requête. Nous voulons utiliser ces données fictives pour Storybook et les tests unitaires, nous utilisons donc le deepmerge
afin que nous puissions étendre les données facilement.
Enfin, nous créerons src/mocks/queries/handlers.ts
:
import { countriesQuery } from './countries'export const handlers = [countriesQuery()]
Installons ensuite Storybook.
Pour une configuration rapide, nous allons exécuter la CLI Storybook :
npx storybook init
Une fois terminé le .storybook
répertoire, il doit être créé comme suit :
.storybook/
├── main.js
└── preview.js
Puisque nous utilisons TypeScript dans notre application, nous devons ajouter le alias
entrées correspondant à la paths
dans notre tsconfig.ts
pour charger les modules. Pour ce faire, nous allons installer le tsconfig-paths-webpack-plugin brancher:
yarn add -D tsconfig-paths-webpack-plugin
Ouvrez ensuite un .storybook
et modifiez la configuration Webpack :
const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin');module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"storybook-addon-next-router",
],
framework: "@storybook/react",
core: {
"builder": "@storybook/builder-webpack5"
},
features: {
emotionAlias: false,
},
webpackFinal: async (config, { configType }) => {
// Add plugins
config.resolve.plugins = [
...(config.resolve.plugins || []),
new TsconfigPathsPlugin({
configFile: './tsconfig.json'
}),
];
// Return the altered config
return config;
},
}
Maintenant que nous pouvons charger des modules depuis src/**
dans les fichiers Storybook.
Installer le fournisseur
Depuis que nous avons utilisé Apollo Client, nous devons configurer le fournisseur dans les histoires.
Créer src/storybook/provider.ts
et définissez le fournisseur avec le code suivant :
import 'src/styles/globals.css'
import { ApolloProvider as ApolloProviderLibs } from '@apollo/client'
import React, { useMemo } from 'react'
import { createApolloClient } from 'src/shared/apollo/client'export const Provider: React.FCWithChildren = (props) => {
return <ApolloProvider>{props.children}</ApolloProvider>
}
const ApolloProvider: React.FCWithChildren = (props) => {
const client = useMemo(
() => createApolloClient({ idToken: 'token' }),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
)
return (
<ApolloProviderLibs client={client}>{props.children}</ApolloProviderLibs>
)
}
Ce fournisseur doit être le même que celui utilisé dans le pages/_app.tsx
:
import 'src/styles/globals.css'
import type { AppProps } from 'next/app'
import { ApolloProvider } from 'src/shared/apollo/ApolloProvider'export default function App({ Component, pageProps }: AppProps) {
return (
<ApolloProvider>
<Component {...pageProps} />
</ApolloProvider>
)
}
Ouvrez ensuite un .storybook/preview.js
et encapsulez un composant d’histoire avec le fournisseur :
import {Provider} from "../src/storybook/Provider";export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
}
export const decorators = [
// Add provider here.
(Story) => (
<Provider>
<Story />
</Provider>
),
];
Maintenant que nous pouvons configurer le fournisseur apollo, un composant de récit peut consommer les données fournies.
Puisque nous utilisons Next.js, nous devons configurer le routeur Next.js dans nos histoires Storybook.
Installons le storybook-addon-next-router
brancher.
yarn add -D storybook-addon-next-router
Ouvrez un .storybook/preview.js
et ajouter le RouterContext.Provider
aux paramètres :
import { RouterContext } from "next/dist/shared/lib/router-context";export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
// Add here
nextRouter: {
Provider: RouterContext.Provider,
},
}
Cela nous permet de configurer le chemin du routeur dans des histoires comme ceci :
export const Example = () => <MyComponentThatHasANextLink />;Example.parameters = {
nextRouter: {
path: "/profile/[id]",
asPath: "/profile/lifeiscontent",
query: {
id: "lifeiscontent",
},
},
};
Pour simuler les requêtes GraphQL dans Storybook, nous installons le msw-storybook-addon brancher.
Installez le msw-storybook-addon avec cette commande :
yarn add -D msw-storybook-addon
Pour démarrer le serveur fictif dans les histoires, nous devons générer un service worker dans le répertoire public. Voici comment procéder :
npx msw init public/
Il va créer mockServiceWorker.js
sous le répertoire public, qui ressemble à ceci :
public/
├── favicon.ico
├── mockServiceWorker.js // Added
└── vercel.svg
Ouvrez un .storybook/main.js
et mettre un staticDirs
et repli dans la configuration Webpack :
const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin');module.exports = {
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
staticDirs: ['../public'], // Add staticDirs here.
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"storybook-addon-next-router",
],
framework: "@storybook/react",
core: {
"builder": "@storybook/builder-webpack5"
},
features: {
emotionAlias: false,
},
webpackFinal: async (config, { configType }) => {
config.resolve.plugins = [
...(config.resolve.plugins || []),
new TsconfigPathsPlugin({
configFile: './tsconfig.json'
}),
];
// Add fallback here.
config.resolve = {
...config.resolve,
fallback: {
timers: false,
tty: false,
os: false,
http: false,
https: false,
zlib: false,
util: false,
stream: false,
...config.resolve.fallback,
}
}
// Return the altered config
return config;
},
}
Alors ouvrez .storybook/preview.js
activer le MSW :
import { RouterContext } from "next/dist/shared/lib/router-context";
import * as msw from 'msw-storybook-addon';
import {handlers as queryHandlers} from "../src/mocks/queries/handlers";// Initialize the msw
msw.initialize()
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
nextRouter: {
Provider: RouterContext.Provider,
},
// Add the MSW handlers.
// This will be applied globally.
msw: {
handlers: [...queryHandlers]
}
}
// Provide the MSW addon decorator globally
export const decorators = [
msw.mswDecorator,
(Story) => (
<Provider>
<Story />
</Provider>
),
];
Nous initialisons le MSW et fournissons les gestionnaires que nous avons définis dans src/mocks/queries/handlers.ts
nous pourrons donc nous moquer des données dans les histoires.
Maintenant que nous avons configuré Storybook et le serveur MSW, créons une histoire et voyons comment cela fonctionne.
Créer src/pages/Countries/Countries.stories.tsx
:
import { ComponentStory, ComponentMeta } from '@storybook/react'
import React from 'react'
import { Countries as Page } from './Countries'export default {
title: 'Pages/Countries',
component: Page,
parameters: {
layout: 'fullscreen',
nextRouter: {
asPath: '/countries',
path: '/countries',
},
},
} as ComponentMeta<typeof Page>
const Template: ComponentStory<typeof Page> = (args) => <Page {...args} />
export const Default = Template.bind({})
Démarrez le livre d’histoires avec le code suivant :
yarn storybook
Aller à http://localhost:6006
alors vous pouvez voir que la page des pays obtient avec succès des données simulées et affiche une liste.
Avant de publier, inscrivez-vous Chromatique et créer un projet afin que nous puissions obtenir un jeton de projet unique.
Une fois inscrit, vous verrez une page de configuration de projet comme celle-ci :
Nous suivrons les instructions. Installez le package chromatique avec cette commande :
yarn add -D chromatic
Publiez notre Storybook en utilisant le code suivant :
npx chromatic --project-token=<your token>
Une fois terminé, vous verrez la page de réussite. Il ressemble à ceci :
Maintenant que nous avons publié Storybook sur Chromatic, voyons comment automatiser les tests d’interface utilisateur pour détecter les bogues avec CI. Chromatic fournit une action GitHub pour vous aider à automatiser un test de régression visuelle.
Configurer les secrets
Pour sécuriser notre application, nous stockerons le jeton du projet dans GitHub Secrets.
Aller à Settings
> Secrets
> actions
puis cliquez sur le New repository secret
:
Puis ajouter CHROMATIC_PROJECT_TOKEN
et remplacez la valeur par le jeton de projet généré ci-dessus :
Ajouter un workflow de développement
Nous allons créer un flux de travail qui publie Storybook sur Chromatic et crée un commentaire contenant un lien vers Storybook et Chromatic. Voyons voir comment ça fonctionne.
Créer .github/workflows/chromatic_dev.yml
:
name: Chromatic development
on:
pull_request:
branches: [main]jobs:
chromatic:
name: chromatic development
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: 16.18.1
- name: Cache node_modules
uses: actions/cache@v2
id: node_modules_cache_id
with:
path: node_modules
key: v1-yarn-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
restore-keys: |
v1-yarn-
- name: Run install
if: steps.node_modules_cache_id.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile --silent
- name: Run codegen
run: yarn codegen
- name: Publish to Chromatic
uses: chromaui/action@v1
id: chromatic
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitZeroOnChanges: true
- name: Remove unnecessary path for Chromatic link
id: storybook-url
run: echo "::set-output name=value::${STORYBOOK_URL//\/iframe.html/}"
env:
STORYBOOK_URL: ${{ steps.chromatic.outputs.storybookUrl }}
- name: Find Comment
uses: peter-evans/find-comment@v2
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: ':books: Storybook :books:'
- name: Get datetime for now
id: datetime
run: echo "::set-output name=value::$(date)"
env:
TZ: Asia/Tokyo
- name: Create or update comment
uses: peter-evans/create-or-update-comment@v2
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
Visit the :books: **Storybook** :books: for this PR (updated for commit ${{ github.event.pull_request.head.sha }}):
<${{ steps.storybook-url.outputs.value }}>
<sub>Build URL: ${{ steps.chromatic.outputs.buildUrl }}</sub>
<sub>(:fire: updated at ${{ steps.datetime.outputs.value }})</sub>
edit-mode: replace
Validez le fichier et poussez-le, puis le flux de travail démarrera.
Une fois le flux de travail terminé, vous obtiendrez un commentaire qui ressemble à ça:
Chaque fois que vous poussez un nouveau commit, il publiera Storybook et le commentaire sera mis à jour avec le nouveau changement.
Tests d’interface utilisateur
Ajoutons quelques modifications et voyons comment fonctionnent les tests d’interface utilisateur.
S’ouvrir src/pages/Countries/Countries.tsx
et supprimer du code :
>
<h2>{c.name}</h2>
<p>capital: {c.capital}</p>
- <p>currency: {c.currency}</p>
</a>
))}
</div>
Une fois publié, Chromatic a détecté les changements comme suit :
Vous pouvez voir le PR ici.
Ajouter le workflow principal
Chromatic fournit vos propres permaliens correspondant aux branches et aux commits. Le format du permalien est :
— <appid>.chromatic.com
Vous pouvez obtenir le permalien sur Manage
> Permalinks
:
Une fois fusionné un PR dans la branche principale, nous voulons le publier sur Chromatic afin que nous puissions le lier au dernier Storybook de la branche principale. Pour cela, nous allons créer un workflow pour la branche principale.
Créer .github/workflows/chromatic_main.yml
:
name: Chromatic main
on:
push:
branches: [main]jobs:
chromatic:
name: chromatic main
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-node@v2
with:
node-version: 16.18.1
- name: Cache node_modules
uses: actions/cache@v2
id: node_modules_cache_id
with:
path: node_modules
key: v1-yarn-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
restore-keys: |
v1-yarn-
- name: Run install
if: steps.node_modules_cache_id.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile --silent
- name: Run codegen
run: yarn codegen
- name: Publish to Chromatic
uses: chromaui/action@v1
id: chromatic
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
autoAcceptChanges: true
La autoAcceptChanges
L’option doit être vraie car nous vérifions déjà les modifications dans PR, alors publiez-la.
Une fois terminé, le dernier Storybook sera disponible sur — <appid>.chromatic.com
.
Enfin, nous allons implémenter des tests unitaires avec le serveur MSW.
Installez le @testing-library/react
forfait:
yarn add -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
Installez le package jest et d’autres packages :
yarn add -D jest jest-environment-jsdom @swc/core @swc/jest @types/jest node-fetch@2.6.6 empty
Créer jest.config.js
:
module.exports = {
testEnvironment: 'jsdom',
globals: {
__DEV__: true,
},
testMatch: ['**/src/**/?(*.)+(spec|test).[jt]s?(x)'],
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
transform: {
'^.+\\.(js|jsx|ts|tsx)$': ['@swc/jest'],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'd.ts'],
moduleNameMapper: {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|css)$':
'empty/object',
'^src/(.*)$': '<rootDir>/src/$1',
},
}
Créer setupTests.js
:
import '@testing-library/jest-dom/extend-expect'global.fetch = require('node-fetch')
jest.mock('src/config')
jest.retryTimes(3, { logErrorsBeforeRetry: true })
// @see https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
})
Créer .swcrc
:
{
"jsc": {
"target": "es2015",
"parser": {
"syntax": "typescript",
"tsx": true
},
"transform": {
"react": {
"runtime": "automatic",
"pragma": "React.createElement",
"pragmaFrag": "React.Fragment",
"throwIfNamespace": true,
"useBuiltins": true
}
}
},
"sourceMaps": true
}
Créer src/mocks/server.ts
:
import { setupServer } from 'msw/node'
import { handlers as queryHandlers } from './queries/handlers'export const server = setupServer(...[...queryHandlers])
export const removeAllListeners = () => {
server.events.removeAllListeners()
}
Nous créerons src/testUtils
et structure comme ceci:
src/testUtils/
├── Provider.tsx
├── index.ts
└── mock
└── setup.ts
Créer src/testUtils/Provider.tsx
:
import React, { useMemo } from 'react'
import { createApolloClient } from 'src/shared/apollo/client'
import { ApolloProvider as ApolloProviderLibs } from '@apollo/client'export const Provider: React.FCWithChildren = (props) => {
return <ApolloProvider>{props.children}</ApolloProvider>
}
const ApolloProvider: React.FCWithChildren = (props) => {
const client = useMemo(() => createApolloClient({ idToken: 'token' }), [])
return (
<ApolloProviderLibs client={client}>{props.children}</ApolloProviderLibs>
)
}
Créer src/testUtils/mock/setup.ts
:
import { server } from 'src/mocks/server'type Callback = () => void
export const startServer = (callback?: Callback) => {
beforeAll(async () => {
if (callback) await callback()
server.listen()
})
}
export const resetServer = (callback?: Callback) => {
afterAll(async () => {
if (callback) await callback()
server.resetHandlers()
})
}
export const resetHandlers = (callback?: Callback) => {
afterEach(async () => {
if (callback) await callback()
server.resetHandlers()
})
}
export const closeServer = (callback?: Callback) => {
afterAll(async () => {
if (callback) await callback()
server.close()
})
}
Nous allons nous moquer de la requête GraphQL en utilisant ces fonctions dans les tests.
créons src/pages/Countries/Countries.test.tsx
:
import { render, screen } from '@testing-library/react'
import React from 'react'
import { removeAllListeners } from 'src/mocks/server'
import { Provider } from 'src/testUtils'
import {
closeServer,
resetHandlers,
resetServer,
startServer,
} from 'src/testUtils/mock/setup'
import { Countries as Component } from '../Countries'type Props = {}
const propsData = (options?: Partial<Props>): Props => ({
...options,
})
describe('pages/Countries', () => {
startServer()
closeServer()
resetServer()
resetHandlers()
beforeEach(async () => {
removeAllListeners()
})
describe('Countries', () => {
test('renders countries list', async () => {
render(
<Provider>
<Component {...propsData()} />
</Provider>,
)
expect(await screen.findByText('Argentina')).toBeInTheDocument()
})
})
})
Ensuite, il devrait réussir le test. Voici à quoi cela ressemble :
PASS src/pages/Countries/__tests__/Countries.test.tsx
pages/Countries
Countries
✓ renders countries list (67 ms)Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.247 s, estimated 5 s
Ran all test suites.
✨ Done in 2.48s.
Lorsque nous voulons changer la réponse, nous la moquons dans le test en utilisant le server.use
:
//...
import { removeAllListeners, server } from 'src/mocks/server'
import { countriesQuery } from 'src/mocks/queries/countries'test('renders Japan', async () => {
// Change a reponse against Countries query.
// Countries list will be deeply merged with the new value.
server.use(
countriesQuery({
res: {
countries: [
{
name: 'Japan',
native: '日本',
capital: 'Tokyo',
emoji: '🇯🇵',
currency: 'JPY',
languages: [
{
code: 'ja',
name: 'Japanese',
},
],
},
],
},
}),
)
render(
<Provider>
<Component {...propsData()} />
</Provider>,
)
expect(await screen.findByText('Japan')).toBeInTheDocument()
})
Nous avons expliqué comment introduire des tests de régression visuelle dans notre application. Lors des tests frontend, nous devons tester le comportement réel de l’application et de l’interface utilisateur.
Chromatic détecte automatiquement le changement d’interface utilisateur, vous permettant de refactoriser la base de code sans vous soucier des bogues de l’interface utilisateur. MSW vous permet de tester votre application exactement comment vos utilisateurs interagiraient avec elle sans se moquer du code de l’application.