Визуальное регрессионное тестирование с Chromatic, Storybook и Mock Service Workers
В этой статье вы узнаете, как настроить Chromatic и организовать визуальное регрессионное тестирование в вашем приложении. Вот итоговая база кода на GitHub.
- Chromatic — это облачный набор инструментов для сборника рассказов, который позволяет публиковать сборник рассказов, тестировать и просматривать пользовательский интерфейс. Мы автоматизируем визуальное регрессионное тестирование с помощью Chromatic и GitHub Actions.
- MSW — библиотека, которая имитирует запросы API, перехватывая их на сетевом уровне. MSW позволяет вашему приложению работать без имитации определенного кода в вашей кодовой базе. Мы будем использовать сервер MSW для Storybook и модульных тестов.
Зависимости
Вот зависимости, которые нужно захватить:
- следующий@13.0.6
- реагировать@18.2.0
- аполлон/клиент@3.7.2
- graphql-codegen/[email protected]
- мсв@0.49.2
- сборник рассказов@v6
Обзор
Предположим, у нас есть приложение, созданное Next.js и сервером GraphQL API. Для начала я настроил приложение, которое установило GraphQL, apollo-client и graphql-codegen. Подробности можно посмотреть в этом PR.
Наше приложение делает запрос API на сервер countries.revorblades.com и получает список стран, как показано ниже. Мы проведем визуальный регрессионный тест для этой страницы.
Вот обзор реализации.
- Настройка MSW
- Установить сборник рассказов
- Установите плагин storybook-addon-next-router
- Установите плагин msw-storybook-addon.
- Создавайте истории
- Настройка хроматического
- Автоматизируйте проверку пользовательского интерфейса с помощью GitHub Actions
- Модульные тесты
Настройка MSW
Начнем с установки пакета msw
в наш проект.
yarn add -D msw
Мы также установим пакеты deepmerge
и utility-types
для гибкого расширения фиктивных данных.
yarn add -D deepmerge utility-types
Чтобы модули, связанные с имитацией API, находились в одном каталоге, мы создадим каталог src/mocks
с помощью следующей команды:
mkdir src/mocks
Чтобы определить макет для запросов GraphQL, мы создадим каталог src/mocks/queries
:
mkdir src/mocks/queries
В нашем приложении у нас есть такой запрос Countries
:
query Countries($filer: CountryFilterInput) { countries(filter: $filer) { name native capital emoji currency languages { code name } } }
Чтобы обработать запрос GraphQL, нам нужен распознаватель, который возвращает фиктивный ответ и создает фиктивные данные. Чтобы сгруппировать модули в одну папку, мы будем структурировать файлы следующим образом:
src/mocks/ └── queries ├── countries │ ├── countriesQuery.ts │ ├── data.ts │ ├── index.ts │ └── type.ts └── handlers.ts
Создать 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))) }) }
countriesQuery
возвращает преобразователь ответа на запрос Countries
, который возвращает имитированные данные через функцию res
.
Создайте 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 }
И создайте 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, }, )
Это фиктивные данные, на которые сервер MSW ответит на запрос Countries
. Мы хотим использовать эти фиктивные данные для Storybook и модульных тестов, поэтому мы используем deepmerge
, чтобы мы могли легко расширять данные.
Наконец, мы создадим src/mocks/queries/handlers.ts
:
import { countriesQuery } from './countries' export const handlers = [countriesQuery()]
Установить сборник рассказов
Далее давайте установим Storybook.
Для быстрой настройки запустим CLI Storybook:
npx storybook init
Как только он закончит каталог .storybook
, он должен быть создан следующим образом:
.storybook/ ├── main.js └── preview.js
Поскольку мы используем TypeScript в нашем приложении, нам нужно добавить записи alias
, соответствующие paths
в нашем tsconfig.ts
для загрузки модулей. Для этого установим плагин tsconfig-paths-webpack-plugin:
yarn add -D tsconfig-paths-webpack-plugin
Затем откройте .storybook
и измените конфигурацию 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; }, }
Теперь, когда мы можем загружать модули из src/**
в файлы Storybook.
Установить провайдера
Так как мы использовали Apollo Client, нам нужно настроить провайдера в историях.
Создайте src/storybook/provider.ts
и определите поставщика с помощью следующего кода:
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> ) }
Этот поставщик должен быть таким же, как используемый в 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> ) }
Затем откройте .storybook/preview.js
и оберните компонент истории провайдером:
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> ), ];
Теперь, когда мы можем настроить провайдера apollo, компонент истории может использовать предоставленные данные.
Установите плагин storybook-addon-next-router.
Поскольку мы используем Next.js, нам нужно настроить маршрутизатор Next.js в наших историях из сборника рассказов.
Давайте установим плагин storybook-addon-next-router
.
yarn add -D storybook-addon-next-router
Откройте .storybook/preview.js
и добавьте RouterContext.Provider
к параметрам:
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, }, }
Это позволяет нам настроить путь маршрутизатора в таких историях:
export const Example = () => <MyComponentThatHasANextLink />; Example.parameters = { nextRouter: { path: "/profile/[id]", asPath: "/profile/lifeiscontent", query: { id: "lifeiscontent", }, }, };
Установите плагин msw-storybook-addon.
Для имитации запросов GraphQL внутри Storybook мы устанавливаем плагин msw-storybook-addon.
Установите msw-storybook-addon с помощью этой команды:
yarn add -D msw-storybook-addon
Чтобы запустить фиктивный сервер в историях, нам нужно сгенерировать сервис-воркер в публичном каталоге. Вот как это сделать:
npx msw init public/
Он создаст mockServiceWorker.js
в общедоступном каталоге, который выглядит следующим образом:
public/ ├── favicon.ico ├── mockServiceWorker.js // Added └── vercel.svg
Откройте .storybook/main.js
и поместите staticDirs
и запасной вариант в конфигурацию 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; }, }
Затем откройте .storybook/preview.js
включите 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> ), ];
Мы инициализируем MSW и предоставляем обработчики, которые мы определили в src/mocks/queries/handlers.ts
, чтобы мы могли имитировать данные в историях.
Создавайте истории
Теперь, когда мы настроили Storybook и сервер MSW, давайте создадим историю и посмотрим, как она работает.
Создайте 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({})
Запустите сборник рассказов с помощью следующего кода:
yarn storybook
Перейдите к https://localhost:6006
, и вы увидите, что страница стран успешно получает имитированные данные и отображает список.
Настройка хроматического
Перед публикацией зарегистрируйтесь в Chromatic и создайте проект, чтобы мы могли получить уникальный токен проекта.
После регистрации вы увидите страницу настройки проекта, подобную этой:
Мы будем следовать инструкциям. Установите хроматический пакет с помощью этой команды:
yarn add -D chromatic
Опубликуйте наш сборник рассказов, используя следующий код:
npx chromatic --project-token=<your token>
По завершении вы увидите страницу успеха. Это выглядит так:
Автоматизируйте проверку пользовательского интерфейса с помощью GitHub Actions
Теперь, когда мы опубликовали Storybook to Chromatic, давайте посмотрим, как автоматизировать тесты пользовательского интерфейса для выявления ошибок с помощью CI. Chromatic предоставляет действие GitHub, которое поможет вам автоматизировать визуальный регрессионный тест.
Настройка секретов
Чтобы защитить наше приложение, мы будем хранить токен проекта в GitHub Secrets.
Перейдите к Settings
› Secrets
› actions
, затем нажмите New repository secret
:
Затем добавьте CHROMATIC_PROJECT_TOKEN
и замените значение сгенерированным токеном проекта выше:
Добавить рабочий процесс разработки
Мы создадим рабочий процесс, который публикует Storybook в Chromatic и создает комментарий со ссылкой на Storybook и Chromatic. Посмотрим, как это работает.
Создайте .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
Зафиксируйте файл и нажмите его, после чего начнется рабочий процесс.
После завершения рабочего процесса вы получите комментарий выглядит так:
Каждый раз, когда вы отправляете новую фиксацию, она публикует Storybook, а комментарий будет обновляться с учетом нового изменения.
тесты пользовательского интерфейса
Давайте добавим некоторые изменения и посмотрим, как работают UI-тесты.
Откройте src/pages/Countries/Countries.tsx
и удалите код:
> <h2>{c.name}</h2> <p>capital: {c.capital}</p> - <p>currency: {c.currency}</p> </a> ))} </div>
После публикации Chromatic обнаружил изменения следующим образом:
Посмотреть PR можно здесь.
Добавьте основной рабочий процесс
Chromatic предоставляет ваши собственные постоянные ссылки, соответствующие веткам и коммитам. Формат постоянной ссылки:
https://<branch> — <appid>.chromatic.com
Вы можете получить постоянную ссылку на Manage
› Permalinks
:
После слияния PR с основной веткой мы хотим опубликовать его в Chromatic, чтобы мы могли ссылаться на последний сборник рассказов основной ветки. Для этого мы создадим рабочий процесс для основной ветки.
Создайте .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
Параметр autoAcceptChanges
должен быть верным, потому что мы уже проверяем изменения в PR, поэтому публикуем его.
После завершения последний сборник рассказов будет доступен по адресу https://main — <appid>.chromatic.com
.
Модульные тесты
Наконец, мы реализуем модульные тесты с сервером MSW.
Установите пакет @testing-library/react
:
yarn add -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
Установите jest и другие пакеты:
yarn add -D jest jest-environment-jsdom @swc/core @swc/jest @types/jest [email protected] empty
Создайте 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', }, }
Создайте 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(), })), })
Создайте .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 }
Создайте 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() }
Мы создадим src/testUtils
и структурируем так:
src/testUtils/ ├── Provider.tsx ├── index.ts └── mock └── setup.ts
Создайте 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> ) }
Создайте 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() }) }
Мы будем имитировать запрос GraphQL, используя эти функции внутри тестов.
Создадим 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() }) }) })
Затем он должен пройти тест. Вот как это выглядит:
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.
Когда мы захотим изменить ответ, мы смоделируем его внутри теста, используя 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() })
Заключение
Мы рассмотрели, как внедрить визуальное регрессионное тестирование в наше приложение. При внешнем тестировании мы должны проверить фактическое поведение приложения и пользовательского интерфейса.
Chromatic автоматически обнаруживает изменение пользовательского интерфейса, позволяя вам проводить рефакторинг кодовой базы, не беспокоясь об ошибках пользовательского интерфейса. MSW позволяет протестировать ваше приложение именно так, как ваши пользователи будут с ним взаимодействовать, не имитируя код приложения.