Попробуйте фронтенд-тестирование! (Реагировать на бой)

внешний интерфейс

предисловие

Прошу прощения, наконец-то вышел настоящий бой, который долгое время был голубиным!

Эта статья является частью серии автоматизированных интерфейсных тестов.ReactВ реальной боевой части серия автоматизированного тестирования перейдет от теории к практике и действительно приведет всех к тому, чтобы научиться использовать интерфейс автоматизированного тестирования и внедрять его в бизнес.

Прочитав всю серию, вы не будете использовать инструменты автоматизированного тестирования для повышения эффективности производства, приходите ко мне!

Старые правила, более 200 лайков, продолжайте обновлятьVueУчебник по совмещению с автоматизированным тестированием.

обрати внимание на"Привет ФЕ"Получите больше простых практических руководств.

Данная статья является практическим пособием, если в статье есть ошибки в теоретических знаниях, просьба указывать их в комментариях!

После научно-популярного в предыдущей статье у каждого должно быть определенное представление об автоматизированном тестировании интерфейса.

Студенты, которые не читали предыдущую статью о концепциях, связанных с автоматизированным тестированием и базовой грамматикой Jest, могут нажать на портал:«Не хотите потерять общую зарплату и премию по итогам года? Попробуйте автоматизированное тестирование! (Базовый)"

Я надеюсь, что каждый сможет сначала заложить основу, а затем приступить к настоящей боевой части!

Выкладываю код собственно боевой части в свой Git-репозиторий:wjq990112 / Learing-React-Test, вы можете нажать на маленькую звездочку ⭐️ и продолжать следить за последующими обновлениями~

Предварительное знание

Я надеюсь, что вы немного умеете читать документы на английском языке.

Так как в Китае не так много практики по фронтенд-автоматизации, мало статей по теме и не хватает материалов на китайском языке, многие библиотечные классы можно изучить и использовать, только читая официальные англоязычные документы. Конечно, в этой статье я постараюсь дать вам документы на английском языке.Основание для сравнения важнеечасть объяснить.

Я надеюсь, у тебя есть немногоReactБаза

Так как это сReactВ сочетании с автоматизированной практикой тестирования, тоReactОсновное использование является обязательным, еслиReactСтуденты, которые еще этого не знают, вы можете купить@神三元(Саньюань не забывайте зарабатывать деньги 🐶) буклет:«React Hooks и неизменяемый поток данных в действии», после прочтения вы также можете статьReactвладелец.

Я надеюсь, что вы немного знакомы с основами TypeScript.

Хотя я постараюсь свести к минимуму использование синтаксиса TypeScript в реальном бою, не смущайтесь, когда используется какой-то код!

Надеюсь, у вас есть инженерные навыки

Автоматизированное тестирование — это инженерная проблема, и вы должны иметь определенное представление об этом аспекте. В статье будут рассмотрены некоторыеbabel.config.js,jest.config.jsВ ожидании конфигурации и объяснения конфигурационных файлов студенты, не владеющие основами настройки, могут сначала изучить содержание статьи, а затем они должны сами прочитать официальные документы, чтобы учиться!

Готов к работе

метод первый

Настоятельно рекомендуется следовать этому методу шаг за шагом, чтобы создать настоящую боевую среду!

каждый может использоватьcreate-react-appСоздайте проект самостоятельно двумя способами:

npx create-react-app jest-react --template-typescript

или:

npm install create-react-app -g
create-react-app jest-react --template-typescript

После того, как вы создали свой проект, вы можете начать экспериментировать с автоматическим тестированием иReactВолшебный эффект комбинации!

Способ второй

Способ 1. Если вы сочтете это хлопотным, вы также можете напрямую получить мой код из GitHub и выбрать ветку, соответствующую учебнику:

# 基础教程
git checkout base
# 进阶教程: testing-library
git checkout advance/testing-library
# 进阶教程: enzyme
git checkout advance/enzyme

Затем выполните:

npm install
npm run start

служба работает наhttp://localhost:3000, браузер откроется автоматически, и после запуска сервиса вы сможете увидеть интерфейс реального проекта!

Конечно, наш проект в основном для объяснения автоматизированного тестирования, интерфейс не такой красивый. Он направлен на то, чтобы каждый мог изучить автоматизированное тестирование переднего плана с помощью реального боя с кодом.Если вы хотите сделать интерфейс более красивым, вы можете вытащить код и добавить несколько стилей!

Тем не менее, я все же рекомендую вам использовать метод шаг за шагом, чтобы вы были более впечатлены и помогли вам быстрее изучить и понять содержание этой статьи!

Базовый учебник

разработка компонента

Как только мы закончим с подготовкой, мы можем начать писать код.

Как обычно, делайте это медленно в первый раз, сначала привыкните к этому и не применяйте слишком много силы:

Давайте сначала напишем HelloWorld, надеясь, что это принесет удачу нашим дальнейшим исследованиям!

// App.tsx
import React, { useState } from 'react';
import './App.css';

function App() {
  const [content, setContent] = useState('Hello World!');

  return (
    <div
      className="app"
      // 方便测试用例中获取 DOM 节点
      data-testid="container"
      onClick={() => {
        setContent('Hello Jack!');
      }}
    >
      {content}
    </div>
  );
}

export default App;

Очень простой компонент, один щелчок станетHello Jack!:

HelloWorld

Написание тестовых случаев

Прежде всего, мы должны подумать о том, какие части нашего компонента HelloWorld необходимо протестировать?

Давайте подумаем об этом с точки зрения пользователя:

  1. ВидетьHello World!
  2. нажмитеHello World!
  3. ВидетьHello Jack!

Затем нам нужно, чтобы этот процесс прошел, нам нужно пройти следующие шаги:

  1. Компонент HelloWorld отображается нормально,divСодержимое этикеткиHello World!
  2. childrenзаHello World!изdivВкладка нажата
  3. divпомеченchildrenстатьHello Jack!

Нарисуйте блок-схему:

HelloWorld

Найдено нет, способ мышления, который мы используем здесь,BDD, не понимаютBDDстуденты, вы можете вернуться и прочитать мою последнюю статью:портал.

Итак, теперь, когда мы определили точки действия, необходимые для запуска теста, мы можем приступить к написанию тестового кода, здесь мы используемReactофициальный дефолтReact Testing Library.

При создании проекта у нас настроен React по умолчанию, в базовом туториале нам не нужно делать ручное тестирование.

Когда мы создаем проект, мы обнаружим, что вsrcЕсть каталогApp.test.tsxфайл, откройте его, и мы найдем очень простой тестовый пример:

// App.test.tsx
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
  // render 方法返回一个包裹对象 对象中包括一些对 DOM 的查询/获取方法
  // getByText: 通过标签的 text 获取 DOM
  const { getByText } = render(<App />);
  // 获取 text 匹配正则 /learn react/i 的 DOM
  const linkElement = getByText(/learn react/i);
  // 判断 DOM 是否在 Document 中
  expect(linkElement).toBeInTheDocument();
});

Это самое простое демо, я прокомментировал весь код, который может быть непонятен, более подробный контент еще впереди.React Testing Libraryполучить из документации.

запускать тестовые случаи

Давайте сначала выполним его на консоли

npm run test

Вы найдете такой результат:

报错

Это потому чтоgetByTextIdЭтот API вызовет исключение, если соответствующий узел DOM не будет найден.

Теперь давайте удалим эту часть и заменим ее тестовым случаем, который нам нужно написать:

// App.test.tsx
import React from 'react';
import { render, fireEvent, RenderResult } from '@testing-library/react';
import App from './App';

let wrapper: RenderResult;

// 运行每一个测试用例前先渲染组件
beforeEach(() => {
  wrapper = render(<App />);
});

describe('Should render App component correctly', () => {
  // 初始化文本内容为 "Hello World!"
  test('Should render "Hello World!" correctly', () => {
    // getByTestId: 通过属性 data-testid 来获取对应的  DOM
    // 这里我们获取到的是上面 HelloWorld 组件中的 div 标签
    const app = wrapper.getByTestId('container');
    expect(app).toBeInTheDocument();
    // 判断获取到的标签是否是 div
    expect(app.tagName).toEqual('DIV');
    // 判断 div 标签的 text 是否匹配正则 /world/i
    expect(app.textContent).toMatch(/world/i);
  });

  // 点击后文本内容为 "Hello Jack!"
  test('Should render "Hello Jack!" correctly after click', () => {
    const app = wrapper.getByTestId('container');
    // fireEvent: 模拟点击事件
    fireEvent.click(app);
    expect(app.textContent).toMatch(/jack/i);
  });
});

Здесь могут быть некоторые студенты, которые запутались, это не имеет значения, давайте посмотрим на наш тестовый код построчно.

let wrapper: RenderResult;

// 运行每一个测试用例前先渲染组件
beforeEach(() => {
  wrapper = render(<App />);
});

beforeEachХуки жизненного цикла запускаются перед каждым тестовым случаем, здесь мы визуализируем компонент HelloWorld вnodeВ смоделированной выше среде jsdom на узле фактически смоделирован браузер.

мы используемwrapperПеременные для сохранения наших результатов рендеринга, затем библиотека тестирования реагирования на некоторые из методов, которые мы инкапсулировали, чтобы получить соответствующие элементы DOM.

describe('Should render App component correctly', () => {});

Увидев это, некоторые студенты могут снова запутаться.На самом деле это легко понять, а буквально — добавить описание к блоку, который тестирует большой тест.

Конечно, вы также можете написать два тестовых примера без записи. Если не написано,JestПо умолчанию имя файла используется в качестве описания тестового модуля.

Наши тестовые примеры написаны на

test('测试用例的描述', () => {});

в функции обратного вызова каждыйtestФункция — это тестовый пример.

testфункция также имеет псевдонимit, если вы увидите позже

it('测试用例的描述', () => {});

И не удивляйтесь, узнав, что этоtestВот и все.

Вышеупомянутая часть туториала является дополнением к той части, которая не была упомянута в предыдущей статье, чтобы друзья, прочитавшие статью, не знали причины этого.

Теперь мы бежим

npm run test

Результат становится таким:

通过

Разве не здорово видеть ряд зеленых результатов?

сделай это снова

Теперь давайте обновим его и покажем вам официальный базовый пример библиотеки тестирования React:

// hidden-message.js
import React from 'react';

// NOTE: React Testing Library works with React Hooks _and_ classes just as well
// and your tests will be the same however you write your components.
function HiddenMessage({ children }) {
  const [showMessage, setShowMessage] = React.useState(false);
  return (
    <div>
      <label htmlFor="toggle">Show Message</label>
      <input
        id="toggle"
        type="checkbox"
        onChange={(e) => setShowMessage(e.target.checked)}
        checked={showMessage}
      />
      {showMessage ? children : null}
    </div>
  );
}

export default HiddenMessage;
// __tests__/hidden-message.js
// these imports are something you'd normally configure Jest to import for you
// automatically. Learn more in the setup docs: https://testing-library.com/docs/react-testing-library/setup#cleanup
import '@testing-library/jest-dom';
// NOTE: jest-dom adds handy assertions to Jest and is recommended, but not required

import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import HiddenMessage from '../hidden-message';

test('shows the children when the checkbox is checked', () => {
  const testMessage = 'Test Message';
  render(<HiddenMessage>{testMessage}</HiddenMessage>);

  // query* functions will return the element or null if it cannot be found
  // get* functions will return the element or throw an error if it cannot be found
  expect(screen.queryByText(testMessage)).toBeNull();

  // the queries can accept a regex to make your selectors more resilient to content tweaks and changes.
  fireEvent.click(screen.getByLabelText(/show/i));

  // .toBeInTheDocument() is an assertion that comes from jest-dom
  // otherwise you could use .toBeDefined()
  expect(screen.getByText(testMessage)).toBeInTheDocument();
});

Над кучей англоязычных комментариев должны быть друзья, плохо владеющие английским, которые скажут:"Ах, как я могу понять, что вы копируете весь набор на английском~"

Не волнуйтесь, я переведу для вас!

// query* functions will return the element or null if it cannot be found
// get* functions will return the element or throw an error if it cannot be found
expect(screen.queryByText(testMessage)).toBeNull();

Этот фрагмент кода использует API:queryByText, роль этого API заключается в передачеtextчтобы найти соответствующий DOM. Но,queryByTextиgetByTextКакая разница?

Разница в том,query*При вызове типа API, если соответствующий DOM не найден, он вернетnull,ноget*Об ошибке будет сообщено напрямую, если соответствующий DOM не будет найден.

// the queries can accept a regex to make your selectors more resilient to content tweaks and changes.
fireEvent.click(screen.getByLabelText(/show/i));

Этот фрагмент кода, после моего объяснения выше, должен быть знаком для имитации события щелчка. Прежде чем имитировать событие клика, мы должны найти соответствующий DOM,getByLabelTextAPI черезlabelНайдите соответствующий DOM для содержимого, а переданные параметры поддерживают регулярные выражения.

// .toBeInTheDocument() is an assertion that comes from jest-dom
// otherwise you could use .toBeDefined()
expect(screen.getByText(testMessage)).toBeInTheDocument();

Этот фрагмент кода используется для определения того, находится ли DOM вDocumentв, из нихtoBeInTheDocumentAPI естьjest-domПредоставленный метод, этот метод не требуется, вы также можете использоватьjestВстроенный APItoBeDefinedсудить.

Есть так много основных частей настоящего боя, что ученики, которые хотят увидеть продвинутую часть, могут продолжать учиться и смотреть~

Расширенный учебник

всплывающая конфигурация проекта

в процессе подготовкиметод первыйОба способа создания на основе TypeScriptReactпроект. использоватьcreate-react-appВ проекте, созданном скаффолдингом, по умолчанию представлены инструменты для автоматического тестирования, но скаффолдинг по умолчанию скрывает некоторые конфигурации инструментов.Если мы хотим открыть конфигурацию и настроить ее вручную, нам нужно запустить:

npm run eject

Откройте некоторые инженерные конфигурации по умолчанию, и после открытия каталог проекта станет таким:

README.md         node_modules      package.json      scripts
config            package-lock.json public            src

больше, чем когда проект был впервые созданconfigиscriptsДве папки, которые являются некоторыми файлами конфигурации проекта по умолчанию для строительных лесов, если вы мало знаете о разработке,не меняй это!

Помимо еще двух файловых каталогов, вpackage.jsonТакже было добавлено множество зависимостей.babelиjestКонфигурация:

  "jest": {
    "roots": [
      "<rootDir>/src"
    ],
    "collectCoverageFrom": [
      "src/**/*.{js,jsx,ts,tsx}",
      "!src/**/*.d.ts"
    ],
    "setupFiles": [
      "react-app-polyfill/jsdom"
    ],
    "setupFilesAfterEnv": [
      "<rootDir>/src/setupTests.js"
    ],
    "testMatch": [
      "<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
      "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
    ],
    "testEnvironment": "jest-environment-jsdom-fourteen",
    "transform": {
      "^.+\\.(js|jsx|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
      "^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
      "^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
    },
    "transformIgnorePatterns": [
      "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$",
      "^.+\\.module\\.(css|sass|scss)$"
    ],
    "modulePaths": [],
    "moduleNameMapper": {
      "^react-native$": "react-native-web",
      "^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
    },
    "moduleFileExtensions": [
      "web.js",
      "js",
      "web.ts",
      "ts",
      "web.tsx",
      "tsx",
      "json",
      "web.jsx",
      "jsx",
      "node"
    ],
    "watchPlugins": [
      "jest-watch-typeahead/filename",
      "jest-watch-typeahead/testname"
    ]
  },
  "babel": {
    "presets": [
      "react-app"
    ]
  }

все этоReactофициально предоставленbabelиjestКонфигурация по умолчанию, если вы еще не понялиbabelиjest,Не изменяйте европейский!

Перенос конфигурации Jest/Babel

конфигурация помещается вpackage.jsonНе очень удобно эксплуатировать, если вы хотите его модифицировать, вам нужно перейти наpackage.jsonНаходим их посередине, и вытаскиваем по отдельности:

Сначала создайте два файла в корневом каталогеbabel.config.js jest.config.js, а затем соответственноpackage.jsonСкопируйте конфигурацию Babel и конфигурацию Jest в соответствующий файл конфигурации:

// jest.config.js

module.exports = {
  roots: ['<rootDir>/src'],
  collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'],
  setupFiles: ['react-app-polyfill/jsdom'],
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
  testMatch: [
    '<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
    '<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}'
  ],
  testEnvironment: 'jest-environment-jsdom-fourteen',
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': '<rootDir>/node_modules/babel-jest',
    '^.+\\.css$': '<rootDir>/config/jest/cssTransform.js',
    '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)':
      '<rootDir>/config/jest/fileTransform.js'
  },
  transformIgnorePatterns: [
    '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$',
    '^.+\\.module\\.(css|sass|scss)$'
  ],
  modulePaths: [],
  moduleNameMapper: {
    '^react-native$': 'react-native-web',
    '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy'
  },
  moduleFileExtensions: [
    'web.js',
    'js',
    'web.ts',
    'ts',
    'web.tsx',
    'tsx',
    'json',
    'web.jsx',
    'jsx',
    'node'
  ],
  watchPlugins: [
    'jest-watch-typeahead/filename',
    'jest-watch-typeahead/testname'
  ]
};
// babel.config.js
module.exports = {
  presets: ['react-app']
};

Детали конфигурации Jest

Думаю, здесь должен быть еще один одноклассник, который сказал:«Ах, как я могу понять вашу беспорядочную и беспорядочную конфигурацию~»

Не волнуйтесь, давайте пройдемся по ним один за другим!

babel.config.jsСодержание слишком простое, чтобы подробно объяснять, мы в основном говорим оjest.config.js:

roots: ['<rootDir>/src'],

rootsОн используется для указания корневого каталога Jest.Jest будет обнаруживать и запускать тестовые случаи только в корневом каталоге.

collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts'],

srcВ каталоге много файлов, но если мы хотим сгенерировать отчет о тестовом покрытии, некоторые нерелевантные файлы не могут быть засчитаны в покрытие.

collectCoverageFromиспользуется для указания диапазона статистики тестового покрытия:srcвсе подjs,jsx,ts,tsxфайл, исключая.d.tsТип файла объявления.

setupFiles: ['react-app-polyfill/jsdom'],

setupFilesОн используется для указания файла подготовки перед созданием тестовой среды, которая представлена ​​здесь.react-app-polyfill/jsdomрешатьjsdomпроблемы совместимости.

setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],

setupFilesAfterEnvОн используется для указания файла конфигурации, записываемого для каждого тестового файла после создания тестовой среды.

Мы видим, что по умолчаниюsetupTests.tsСодержание следующее:

// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';

Вводится для каждого тестового файла после создания тестовой среды.@testing-library/jest-dom/extend-expect, который обеспечивает больше адаптации для JestReactсовпадения, такие какtoHaveTextContent.

testMatch: [
    '<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
    '<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}'
  ],

testMatchОн используется для настройки правил Jest для соответствия тестовому файлу.Здесь мы видим, что элементы конфигурации заполнены__tests__все под папкуjs,jsx,ts,tsxи с.spec/.testокончаниеjs,jsx,ts,tsxдокумент.

testEnvironment: 'jest-environment-jsdom-fourteen',

testEnvironmentОн должен быть известен по имени, которое используется для указания среды движения тестового примера.

Мы знаем, что Jest работает в среде узла, но наш интерфейсный код работает в среде браузера, поэтому мы должны использовать некоторые методы для имитации среды браузера в среде узла.

здесьReactОфициальная рекомендация - использоватьjest-environment-jsdom-fourteen, Заинтересованные студенты могут искать эту библиотеку, и теперь естьsixteenверсия.

transform: {
    '^.+\\.(js|jsx|ts|tsx)$': '<rootDir>/node_modules/babel-jest',
    '^.+\\.css$': '<rootDir>/config/jest/cssTransform.js',
    '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)':
      '<rootDir>/config/jest/fileTransform.js'
  },

transformиспользуется для настройки модуля обработки файлов.

В процессе тестирования нам действительно нужно удалить CSS и другие статические ресурсы, не связанные с логикой компонента, но иногда нам нужно ввести эти коды в наш код компонента.

Затем при тестировании нам нужно указать какие-то модули для обработки/замены этих файлов, иначе мы можем подумать, что модуль не может быть найден и сообщить об ошибке.

Эта часть кода определяет всеjs,jsx,ts,tsxиспользоватьbabel-jestПлагины выполняют обработку, всеcssиспользование файла<rootDir>/config/jest/cssTransform.jsмодули выполняют обработку, все не-js,jsx,ts,tsx,css,jsonфайл, оба используют<rootDir>/config/jest/fileTransform.jsмодуль для обработки.

Теперь, когда эти два модуля используются, давайте перейдем к модулям, чтобы увидетьReactКак выглядит официальная конфигурация:

// cssTransform.js
'use strict';

module.exports = {
  process() {
    return 'module.exports = {};';
  },
  getCacheKey() {
    // The output is always the same.
    return 'cssTransform';
  }
};

cssTransform.jsВ модуле мы видим, что вместо него по умолчанию используется пустой модуль.cssдокумент.

// fileTransform.js
'use strict';

const path = require('path');
const camelcase = require('camelcase');

// This is a custom Jest transformer turning file imports into filenames.
// http://facebook.github.io/jest/docs/en/webpack.html

module.exports = {
  process(src, filename) {
    const assetFilename = JSON.stringify(path.basename(filename));

    if (filename.match(/\.svg$/)) {
      // Based on how SVGR generates a component name:
      // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
      const pascalCaseFilename = camelcase(path.parse(filename).name, {
        pascalCase: true
      });
      const componentName = `Svg${pascalCaseFilename}`;
      return `const React = require('react');
      module.exports = {
        __esModule: true,
        default: ${assetFilename},
        ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
          return {
            $$typeof: Symbol.for('react.element'),
            type: 'svg',
            ref: ref,
            key: null,
            props: Object.assign({}, props, {
              children: ${assetFilename}
            })
          };
        }),
      };`;
    }

    return `module.exports = ${assetFilename};`;
  }
};

fileTransform.jsВы можете видеть в модуле, что конфигурация по умолчанию — это когда имя файла начинается с.svgВ конце создайтеReactКомпонент SVG и верните его, в противном случае верните имя файла напрямую.

transformIgnorePatterns: [
    '[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$',
    '^.+\\.module\\.(css|sass|scss)$'
  ],

transformIgnorePatternsИспользуется для настройки файлов, которые модуль обработки файлов должен игнорировать.node_modulesвсе под папкуjs,jsx,ts,tsx, игнорирует все файлы модулей CSS.

modulePaths: [],

modulePathsИспользуется для указания пути поиска модуля, по умолчанию он будет вnode_modulesЕсли вам нужно найти модули в других путях к файлам, вы можете указать путь к файлу вручную.

moduleNameMapper: {
    '^react-native$': 'react-native-web',
    '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy'
  },

moduleNameMapperИспользуется для обработки сопоставления модулей.'^react-native$': 'react-native-web'Он настроен для React Native, веб-приложение можно удалить,'^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy'Он предназначен для сопоставления модуля CSS и преобразования модуля CSS в форму пар ключ-значение.

moduleFileExtensions: [
    'web.js',
    'js',
    'web.ts',
    'ts',
    'web.tsx',
    'tsx',
    'json',
    'web.jsx',
    'jsx',
    'node'
  ],

moduleFileExtensionsИспользуется для настройки суффикса файла, который необходимо искать, если онReactодностраничное веб-приложение, которое может удалятьjs,jsx,ts,tsxсуффикс.

watchPlugins: [
  'jest-watch-typeahead/filename',
  'jest-watch-typeahead/testname'
  ],

watchPluginsИспользуется для указания того, что Jest находится вwatchПлагин в моде, используем эту часть конфигурацииReactОфициальная рекомендация в порядке, и нам в принципе не нужно ее менять.

больше конфигурации

На самом деле в Jest есть много очень полезных пунктов настройки, если у вас возникнут проблемы во время теста, вы можете попробовать прочитать официальную документацию:Официальная документация Jest.

начать

Перед написанием этого демо я подумал об этом, если бы оно было слишком уродливым, никто не хотел бы его смотреть, поэтому я решил сделать хорошее замечание, но моя эстетика не очень хороша, подумав об этом, я, наконец, решил поставить мистера ДеллаTODO ListСтиль в перемещении,Пишите логику и тестируйте код сами, гораздо лучше стоять на плечах гигантов.

Первый взгляд на эффект:

TODO List

Я не буду подробно объяснять код компонента, если вы хотите увидеть код компонента, вы можете перейти в репозиторий GitHub.cloneвниз, затем переключитесь наadvance/testing-libraryфилиал.

Давайте сосредоточимся на том, как писать тесты!

Модульное тестирование (тестирование-библиотека)

Во-первых, нам нужно проанализировать, какие функции должен тестировать каждый компонент,этот момент очень важен.

Если вы не укажете функции, которые должен тестировать каждый компонент в начале, высока вероятность чрезмерного тестирования или отсутствия тестовых ветвей!

  • Компонент заголовка

    1. inputсуществует иvalueПусто
    2. inputможет ввести
    3. inputМожно войти, чтобы отправить
    4. inputможно подать послеvalueЗаглушка
  • Компонент списка

    1. Список пуст, элементов списка нет, счетчик в правом верхнем углу существует и имеет значение 0
    2. Список не пустой, есть элементы списка, счетчик в правом верхнем углу существует и является длиной списка, кнопка удаления для элементов списка существует, и элементы списка могут быть удалены
    3. Список не пустой, есть элемент списка, счетчик в правом верхнем углу существует и является длиной списка, а содержимое элемента списка становитсяinput, вы можете изменить содержимое соответствующего элемента списка после нажатия Enter

После уточнения функций, которые нам нужно протестировать, мы можем начать писать код модульного теста, чтобы проверить, может ли каждый компонент работать нормально:

// Header.test.tsx
import React from 'react';
import { render, fireEvent, RenderResult } from '@testing-library/react';
import Header from '../../components/Header';

let wrapper: RenderResult;
let input: HTMLInputElement;
const addUndoItem = jest.fn();

beforeEach(() => {
  wrapper = render(<Header addUndoItem={addUndoItem} />);
  input = wrapper.getByTestId('header-input') as HTMLInputElement;
});

afterEach(() => {
  wrapper = null;
  input = null;
});

describe('Header 组件', () => {
  it('组件初始化正常', () => {
    // input 存在
    expect(input).not.toBeNull();

    // 组件初始化 input value 为空
    expect(input.value).toEqual('');
  });

  it('输入框应该能输入', () => {
    const inputEvent = {
      target: {
        value: 'Learn Jest'
      }
    };
    // 模拟输入
    // 输入后 input value 为输入值
    fireEvent.change(input, inputEvent);
    expect(input.value).toEqual(inputEvent.target.value);
  });

  it('输入框回车后应该能提交并清空', () => {
    const inputEvent = {
      target: {
        value: 'Learn Jest'
      }
    };
    const keyboardEvent = {
      keyCode: 13
    };
    // 模拟回车
    // 调用 addUndoItem props 调用时参数为 input value
    // input value 置空
    fireEvent.change(input, inputEvent);
    fireEvent.keyUp(input, keyboardEvent);
    expect(addUndoItem).toHaveBeenCalled();
    expect(addUndoItem).toHaveBeenCalledWith(inputEvent.target.value);
    expect(input.value).toEqual('');
  });
});
// List.test.tsx
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import List, { IList } from '../../components/List';

describe('List 组件', () => {
  it('组件初始化正常', () => {
    const props: IList = {
      list: [],
      deleteItem: jest.fn(),
      changeStatus: jest.fn(),
      handleBlur: jest.fn(),
      valueChange: jest.fn()
    };

    const wrapper = render(<List {...props} />);
    const count = wrapper.queryByTestId('count');
    // 计数器存在且数值为 0
    expect(count).not.toBeNull();
    expect(count.textContent).toEqual('0');

    const list = wrapper.queryAllByTestId('list-item');
    // 列表项为空
    expect(list).toHaveLength(0);
  });

  it('列表项应该能删除', () => {
    const props: IList = {
      list: [{ status: 'div', value: 'Learn Jest' }],
      deleteItem: jest.fn(),
      changeStatus: jest.fn(),
      handleBlur: jest.fn(),
      valueChange: jest.fn()
    };

    const wrapper = render(<List {...props} />);
    const count = wrapper.queryByTestId('count');
    // 计数器存在且数值为 1
    expect(count).not.toBeNull();
    expect(count.textContent).toEqual('1');

    const list = wrapper.queryAllByTestId('list-item');
    // 列表项不为空
    expect(list).toHaveLength(1);

    const deleteBtn = wrapper.queryAllByTestId('delete-item');
    // 删除按钮不为空
    expect(deleteBtn).toHaveLength(1);
    const e: Partial<React.MouseEvent> = {};
    fireEvent.click(deleteBtn[0], e);
    // 阻止事件冒泡
    expect(props.changeStatus).not.toHaveBeenCalled();
    // deleteItem 被调用 参数为 0
    expect(props.deleteItem).toHaveBeenCalled();
    expect(props.deleteItem).toHaveBeenCalledWith(0);
  });

  it('列表项应该能编辑', () => {
    const props: IList = {
      list: [
        { status: 'div', value: 'Learn Jest' },
        { status: 'input', value: 'Learn Enzyme' }
      ],
      deleteItem: jest.fn(),
      changeStatus: jest.fn(),
      handleBlur: jest.fn(),
      valueChange: jest.fn()
    };

    const wrapper = render(<List {...props} />);
    const list = wrapper.queryAllByTestId('list-item');
    // 第一项未处于编辑状态 第二项处于编辑状态
    expect(list[0].querySelector('[data-testid="input"]')).toBeNull();
    expect(list[1].querySelector('[data-testid="input"]')).not.toBeNull();

    // 点击第一项
    fireEvent.click(list[0]);
    // changeStatus 被调用 参数为 0
    expect(props.changeStatus).toHaveBeenCalled();
    expect(props.changeStatus).toHaveBeenCalledWith(0);

    // 第二项 input 输入
    fireEvent.change(list[1].querySelector('[data-testid="input"]'), {
      target: { value: 'Learn Testing Library' }
    });
    // valueChange 被调用 参数为 1 Learn Enzyme
    expect(props.valueChange).toHaveBeenCalled();
    expect(props.valueChange).toHaveBeenCalledWith(1, 'Learn Testing Library');

    // 第二项 input 框失焦
    fireEvent.blur(list[1].querySelector('[data-testid="input"]'));
    // handleBlur 被调用 参数为 1
    expect(props.handleBlur).toHaveBeenCalled();
    expect(props.handleBlur).toHaveBeenCalledWith(1);
  });
});

Код в части модульного теста в основном представляет собой некоторые сопоставители, представленные в предыдущей статье.Студенты, которые не читали предыдущую статью или все еще запутались, могут вернуться и просмотреть ее:«Попробуйте фронтальное автоматизированное тестирование! (Базовый)", эта часть не будет объясняться подробно.

Интеграционное тестирование (тестирование-библиотека)

Некоторые ученики могут сказать:«Вышеприведенный ваш контент проверяет, могут ли отдельные компоненты работать должным образом. Что, если они не работают должным образом в сочетании?»

Если вы это видите и чувствуете, значит, вы лучше понимаете автоматизированное тестирование.

Модульные тесты имеют предпосылку по умолчанию:Фрагмент кода будет работать, если все его части работают.

Это как шестерня, если все шестерни в шестерне работают, то работает вся шестерня.

Обычно в корпоративной разработке, если мы можем достичь высокого покрытия модульными тестами, нам не нужно писать код интеграционного теста, чтобы проверить, может ли объединенный код нормально работать.

Но здесь немного по-другому, почему ты так говоришь?

Вы обнаружили, что в приведенном выше тестовом коде нет тестаУвеличился ли компонент «Список» после ввода компонента «Заголовок»Эта функция?

Итак, здесь нам нужно использовать интеграционный тест для проверки комбинации двух компонентов, функция всего приложения не является нормальной:

// App.resolved.test.tsx
import React from 'react';
import { render, fireEvent, act, RenderResult } from '@testing-library/react';
import App from '../../App';
import axios from 'axios';

jest.mock('axios');
axios.get.mockResolvedValue({
  data: {
    code: 200,
    data: [
      {
        status: 'div',
        value: '学习 Jest'
      },
      {
        status: 'div',
        value: '学习 Enzyme'
      },
      {
        status: 'div',
        value: '学习 Testing-Library'
      }
    ],
    message: 'success'
  }
});

let wrapper: RenderResult;
let headerInput: HTMLInputElement;
let count: HTMLDivElement;
let list: HTMLLIElement[];
let input: HTMLInputElement[];
let deleteBtn: HTMLDivElement[];

// 运行每一个测试用例前先渲染组件
beforeEach(async () => {
  await act(async () => {
    wrapper = render(<App />);
  });
  headerInput = wrapper.getByTestId('header-input') as HTMLInputElement;
  count = wrapper.queryByTestId('count') as HTMLDivElement;
  list = wrapper.queryAllByTestId('list-item') as HTMLLIElement[];
  input = wrapper.queryAllByTestId('input') as HTMLInputElement[];
  deleteBtn = wrapper.queryAllByTestId('delete-item') as HTMLDivElement[];
});

// 运行后重置
afterEach(() => {
  wrapper = null;
  headerInput = null;
  count = null;
  list = [];
  input = [];
  deleteBtn = [];
});

describe('App 组件(请求成功时)', () => {
  it('组件初始化正常', () => {
    // headerInput 存在
    expect(headerInput).not.toBeNull();

    // 组件初始化 headerInput value 为空
    expect(headerInput.value).toEqual('');

    // 计数器存在且数值为 3
    expect(count).not.toBeNull();
    expect(count.textContent).toEqual('3');

    // 列表项不为空且长度为 3
    expect(list).toHaveLength(3);

    // 没有列表项处于编辑状态
    expect(input).toHaveLength(0);
  });

  it('输入框提交后列表项应该增加', () => {
    fireEvent.change(headerInput, {
      target: { value: '分享自动化测试学习成果' }
    });
    fireEvent.keyUp(headerInput, { keyCode: 13 });

    expect(count.textContent).toEqual('4');
    // 会触发 DOM 变化 需重新查询一次
    list = wrapper.queryAllByTestId('list-item') as HTMLLIElement[];
    expect(list).toHaveLength(4);

    // 最后一项的内容为添加的内容
    expect(list[3]).toHaveTextContent('分享自动化测试学习成果');
  });

  it('列表项删除后应该能减少', () => {
    fireEvent.click(deleteBtn[2]);

    expect(count.textContent).toEqual('2');
    // 会触发 DOM 变化 需重新查询一次
    list = wrapper.queryAllByTestId('list-item') as HTMLLIElement[];
    expect(list).toHaveLength(2);
  });

  it('列表项应该能编辑并提交', () => {
    fireEvent.click(list[2]);
    const editingItemInput = list[2].querySelector(
      '[data-testid="input"]'
    ) as HTMLInputElement;

    // 第一 二项未处于编辑状态 第三项处于编辑状态
    expect(list[0].querySelector('[data-testid="input"]')).toBeNull();
    expect(list[1].querySelector('[data-testid="input"]')).toBeNull();
    expect(editingItemInput).not.toBeNull();

    // 第三项输入
    fireEvent.change(editingItemInput, {
      target: { value: 'Learn Testing Library' }
    });
    expect(editingItemInput.value).toEqual('Learn Testing Library');

    // 失焦后内容被改变
    fireEvent.blur(editingItemInput);
    expect(list[2]).toHaveTextContent('Learn Testing Library');
  });
});

Студенты, которые внимательно прочитали код, могут быть озадачены этими кодами:

// mock api
jest.mock('axios');
axios.get.mockResolvedValue({
  data: {
    code: 200,
    data: [
      {
        status: 'div',
        value: '学习 Jest'
      },
      {
        status: 'div',
        value: '学习 Enzyme'
      },
      {
        status: 'div',
        value: '学习 Testing-Library'
      }
    ],
    message: 'success'
  }
});
// async render
await act(async () => {
  wrapper = render(<App />);
});

Поскольку наше приложение используетaxiosдля запроса локальных данных, которые в конечном итоге отображаются на странице.

В реальной разработке интерфейс, предоставляемый серверной частью, должен вызываться для получения данных или работы с данными, что приводит к очень важному моменту:Тестовый код не должен навязываться бизнес-коду!

На самом деле этоинвазивныйне простоВторжение в бизнес-кодВторжение в бэкэнд-интерфейс.

вышеmock apiКод фактически имитирует возвращаемое значение интерфейса в тестовой среде, потому чтоaxiosизgetметод возвращаетPromise, поэтому мы также должны называтьmockResolvedValueдля имитации интерфейсаresolveрезультат государства.

Назадasync renderСобственно для этогоaxiosобслуживание, потому чтоaxiosИнтерфейс запроса — это асинхронный процесс, если мы используем синхронный рендеринг, асинхронные события будут помещаться в очередь событий и ждать, пока не завершится выполнение синхронного кода.

В этом случае будут случаи, когда асинхронное событие не было выполнено, а тест-кейс уже был запущен, что в конечном итоге может привести к провалу теста.

Конечно, здесь моделируется только интерфейс.resolveсостояние, мы также можем создатьApp.rejected.test.tsxфайл для проверки интерфейсаrejectсостояние (в фактической разработке интерфейсrejectТакже может понадобиться дружеский совет).

Модульное тестирование и интеграционное тестирование (Enzyme)

КромеReactофициально рекомендуетсяTesting-Library,AirbnbКомпания также представила систему тестированияEnzyme, также является очень полезным тестовым фреймворком, с немного отличающимися дизайнерскими идеями, заинтересованные студенты могут перейти в репозиторий GitHub этого проекта для просмотра, не забудьте переключиться наadvance/enzymeфилиал.

EnzymeКод полностью написан учителем Dell, здесь только для справки и обучения~

Модульное тестирование (связанное с хуками)

в настоящее время используетReact HooksВ процессе разработки мы можем извлечь некоторую повторяющуюся логику в публичные.Hooks, это общественноеHooksнадежность тоже важнаTesting-LibraryТакже предоставляет нам специальный тестReact HooksИнструмент:react-hooks-testing-library

использованная литература