Что значит тестирование? Как проверить код JavaScript с помощью Jest? Изучите основы тестирования JavaScript с этим руководством Jest для начинающих!
Оригинальная статья: Valentino Gagliardi — Jest Tutorial for Beginners: Getting Started With Jest for JavaScript Testing
В техническом жаргоне тестирование означает проверку того, что наш код отвечает некоторым ожиданиям. Например: функция с именем «transformer» должна возвращать ожидаемый результат при некоторых входных данных.
Существует много видов тестирования, но в основном короткие тесты делятся на три основные категории:
В этом руководстве Jest мы рассмотрим только модульное тестирование, но в конце статьи вы найдете ресурсы для других типов тестов.
Jest — это JavaScript-тестер (test runner), то есть библиотека JavaScript для создания, запуска и структурирования тестов. Jest распространяется в виде пакета NPM, вы можете установить его в любом проекте JavaScript. Jest — один из самых популярных тестеров в наши дни, который по умолчанию используется для Create React App.
Когда дело доходит до тестирования, даже простой блок кода может парализовать новичков. Самый распространенный вопрос — «Как мне узнать, что надо тестировать?». Если вы пишете веб-приложение, хорошей отправной точкой будет тестирование каждой страницы приложения и каждого взаимодействия с пользователем. Но веб-приложения также состоят из блоков кода, таких как функции и модули, которые также должны быть протестированы. В большинстве случаев есть два сценария:
Что делать? В обоих случаях вы можете помочь себе, рассматривая тесты как куски кода, которые проверяют, дает ли данная функция ожидаемый результат. Вот как выглядит типичный процесс тестирования:
На самом деле, это все. Тестирование больше не будет страшным, если вы будете думать так: входные параметры — ожидаемый результат — оценка результата. Через минуту мы также увидим удобный инструмент для почти точной проверки того, что тестировать. А теперь руки на Jest!
Как и в каждом проекте JavaScript, вам потребуется среда NPM (убедитесь, что в вашей системе установлен Node). Создайте новую папку и инициализируйте проект:
mkdir getting-started-with-jest && cd $_ npm init -y
Затем установите Jest:
npm i jest --save-dev
Давайте также настроим скрипт NPM для запуска наших тестов из командной строки. Откройте package.json и настройте скрипт с именем «test» для запуска Jest:
"scripts": { "test": "jest" },
Теперь все готово!
Как разработчики, мы все любим свободу творчества. Но когда дело доходит до серьезных вещей, большую часть времени у вас не так много привилегий. Чаще всего мы должны следовать спецификациям, то есть письменному или устному описанию того, что строить.
В этом уроке представим что мы получили довольно простую спецификацию от нашего менеджера проекта. Очень важному клиенту нужна функция JavaScript, которая должна фильтровать массив объектов.
Для каждого объекта мы должны проверить свойство с именем «url», и если значение свойства соответствует предоставленному поисковому термину, мы должны включить соответствующий объект в результирующий массив. Будучи опытным разработчиком JavaScript-кода, вы хотите следовать test-driven development, дисциплине, которая требует написания теста с ошибкой перед началом написания кода.
По умолчанию Jest ожидает найти тестовые файлы в папке с именем __tests__ в папке вашего проекта. Создайте новую папку, затем:
cd getting-started-with-jest mkdir __tests__
Затем создайте новый файл с именем filterByTerm.spec.js внутри __tests__. Вы можете спросить, почему расширение включает «.spec.». Это соглашение, заимствованное из Ruby для пометки файла как specification для данной функциональности.
А теперь давайте пройдем тестирование!
Время создать свой первый тест Jest. Откройте filterByTerm.spec.js и создайте тестовый блок:
describe("Filter function", () => { // test stuff });
Наш первый друг — describe, метод Jest для содержания одного или нескольких связанных тестов. Каждый раз, когда вы начинаете писать новый набор тестов для функциональности, поместите его в блок describe. Как вы можете видеть, он принимает два аргумента: строку для описания набора тестов и функцию обратного вызова для переноса реального теста.
Далее мы встретимся с другой функцией под названием test, которая является фактическим тестовым блоком:
describe("Filter function", () => { test("it should filter by a search term (link)", () => { // actual test }); });
На данный момент мы готовы написать тест. Помните, что тестирование — это вопрос входных данных, функций и ожидаемых результатов. Сначала давайте определим простые входные данные, массив объектов:
describe("Filter function", () => { test("it should filter by a search term (link)", () => { const input = [ { id: 1, url: "https://www.url1.dev" }, { id: 2, url: "https://www.url2.dev" }, { id: 3, url: "https://www.link3.dev" } ]; }); });
Далее мы собираемся определить ожидаемый результат. Согласно заданию тестируемая функция должна исключать объекты, свойство url которых не соответствует заданному поисковому запросу. Мы можем ожидать, например, массив с одним объектом, с заданной ссылкой в качестве поискового запроса:
describe("Filter function", () => { test("it should filter by a search term (link)", () => { const input = [ { id: 1, url: "https://www.url1.dev" }, { id: 2, url: "https://www.url2.dev" }, { id: 3, url: "https://www.link3.dev" } ]; const output = [{ id: 3, url: "https://www.link3.dev" }]; }); });
И теперь мы готовы написать реальный тест. Мы будем использовать функцию expect и Jest matcher для проверки того, что наша фиктивная (пока) функция возвращает ожидаемый результат при вызове. Вот тест:
expect(filterByTerm(input, "link")).toEqual(output);
Там образом мы определили как мы должны вызывать поисковую функцию в своем коде:
filterByTerm(inputArr, "link");
В тесте Jest нужно обернуть вызов функции в expect, которые в сочетании с matcher (функция Jest для проверки выходных данных) выполняет фактические тесты. Вот полный код:
describe("Filter function", () => { test("it should filter by a search term (link)", () => { const input = [ { id: 1, url: "https://www.url1.dev" }, { id: 2, url: "https://www.url2.dev" }, { id: 3, url: "https://www.link3.dev" } ]; const output = [{ id: 3, url: "https://www.link3.dev" }]; expect(filterByTerm(input, "link")).toEqual(output); }); });
(Чтобы узнать больше о Jest matchers, ознакомьтесь с документацией).
Запустим наш тест:
npm test
Вы увидите что тест не проходит:
FAIL __tests__/filterByTerm.spec.js Filter function ✕ it should filter by a search term (2ms) ● Filter function › it should filter by a search term (link) ReferenceError: filterByTerm is not defined 9 | const output = [{ id: 3, url: "https://www.link3.dev" }]; 10 | > 11 | expect(filterByTerm(input, "link")).toEqual(output); | ^ 12 | }); 13 | }); 14 |
«ReferenceError: filterByTerm is not defined. Это хорошая вещь на самом деле. Давайте исправим это в следующем разделе!
Чего действительно не хватает, так это реализации filterByTerm. Для удобства мы собираемся создать функцию в том же файле, где живет тест. В реальном проекте вы должны определить функцию в другом файле и импортировать ее в тестовый файл.
Для выполнения теста мы будем использовать встроенную функцию JavaScript под названием filter, которая способна отфильтровывать элементы из массива. Вот минимальная реализация filterByTerm:
function filterByTerm(inputArr, searchTerm) { return inputArr.filter(function(arrayElement) { return arrayElement.url.match(searchTerm); }); }
Вот как это работает: для каждого элемента входного массива мы проверяем свойство «url», сопоставляя его с регулярным выражением методом match. Вот полный код:
function filterByTerm(inputArr, searchTerm) { return inputArr.filter(function(arrayElement) { return arrayElement.url.match(searchTerm); }); } describe("Filter function", () => { test("it should filter by a search term (link)", () => { const input = [ { id: 1, url: "https://www.url1.dev" }, { id: 2, url: "https://www.url2.dev" }, { id: 3, url: "https://www.link3.dev" } ]; const output = [{ id: 3, url: "https://www.link3.dev" }]; expect(filterByTerm(input, "link")).toEqual(output); }); });
Теперь запустите тест снова:
npm test
и увидеть, как он проходит!
PASS __tests__/filterByTerm.spec.js Filter function ✓ it should filter by a search term (link) (4ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 0.836s, estimated 1s
Отличная работа. Но закончили ли мы тестирование? Еще нет. Что нужно, так это чтобы наша функция перестала работать. Добавим тестирование с поисковым термином в верхнем регистре:
function filterByTerm(inputArr, searchTerm) { return inputArr.filter(function(arrayElement) { return arrayElement.url.match(searchTerm); }); } describe("Filter function", () => { test("it should filter by a search term (link)", () => { const input = [ { id: 1, url: "https://www.url1.dev" }, { id: 2, url: "https://www.url2.dev" }, { id: 3, url: "https://www.link3.dev" } ]; const output = [{ id: 3, url: "https://www.link3.dev" }]; expect(filterByTerm(input, "link")).toEqual(output); expect(filterByTerm(input, "LINK")).toEqual(output); // New test }); });
Запустите тест … и он снова не пройдет. Время исправить это снова!
В filterByTerm должны учитываться также условия поиска в верхнем регистре. Другими словами, он должен возвращать соответствующие объекты, даже если поисковый термин представляет собой строку в верхнем регистре:
filterByTerm(inputArr, "link"); filterByTerm(inputArr, "LINK");
Для проверки этого условия мы ввели новый тест:
expect(filterByTerm(input, "LINK")).toEqual(output); // New test
Чтобы сделать это, мы можем настроить регулярное выражение для match:
// return arrayElement.url.match(searchTerm); //
Вместо того, чтобы сразу передавать searchTerm, мы можем создать регулярное выражение что бы оно не учитывало регистр, то есть выражение, которое соответствует независимо от регистра строки. Вот исправление:
function filterByTerm(inputArr, searchTerm) { const regex = new RegExp(searchTerm, "i"); return inputArr.filter(function(arrayElement) { return arrayElement.url.match(regex); }); }
И вот полный тест:
describe("Filter function", () => { test("it should filter by a search term (link)", () => { const input = [ { id: 1, url: "https://www.url1.dev" }, { id: 2, url: "https://www.url2.dev" }, { id: 3, url: "https://www.link3.dev" } ]; const output = [{ id: 3, url: "https://www.link3.dev" }]; expect(filterByTerm(input, "link")).toEqual(output); expect(filterByTerm(input, "LINK")).toEqual(output); }); }); function filterByTerm(inputArr, searchTerm) { const regex = new RegExp(searchTerm, "i"); return inputArr.filter(function(arrayElement) { return arrayElement.url.match(regex); }); }
Запустите его снова и убедитесь что оно проходит. Отличная работа! В качестве упражнения для вас напишите два новых теста и проверьте следующие условия:
Как бы вы структурировали эти новые тесты?
В следующем разделе мы увидим еще одну важную тему в тестировании: покрытие кода.
Что такое покрытие кода? Прежде чем говорить об этом, давайте сделаем небольшую корректировку нашего кода. Создайте в корневом каталоге вашего проекта новую папку с именем src и создайте файл с именем filterByTerm.js, в который мы поместим и экспортируем нашу функцию:
mkdir src && cd _$ touch filterByTerm.js
Вот содержимое файла filterByTerm.js:
function filterByTerm(inputArr, searchTerm) { if (!searchTerm) throw Error("searchTerm cannot be empty"); const regex = new RegExp(searchTerm, "i"); return inputArr.filter(function(arrayElement) { return arrayElement.url.match(regex); }); } module.exports = filterByTerm;
Теперь давайте представим, что я ваш новый коллега. Я ничего не знаю о тестировании и вместо того, чтобы разузнать как она работает и тестируется, при возникновение ошибки в приложение я сразу иду прямо в эту функцию и добавляю новый оператор if:
function filterByTerm(inputArr, searchTerm) { if (!searchTerm) throw Error("searchTerm cannot be empty"); if (!inputArr.length) throw Error("inputArr cannot be empty"); // new line const regex = new RegExp(searchTerm, "i"); return inputArr.filter(function(arrayElement) { return arrayElement.url.match(regex); }); } module.exports = filterByTerm;
В filterByTerm есть новая строка кода, и, похоже, она не будет протестирована. Если я не скажу вам «есть новый оператор для тестирования», вы не будете точно знать, что тестировать в нашей функции. Почти невозможно представить все пути, по которым может идти наш код, и поэтому возникает необходимость в инструменте, который помогает выявить эти слепые зоны.
Этот инструмент называется покрытием кода, и он является мощным инструментом в нашем наборе инструментов. Jest имеет встроенное покрытие кода, и вы можете активировать его двумя способами:
Перед запуском теста с покрытием обязательно импортируйте filterByTerm в tests/filterByTerm.spec.js:
const filterByTerm = require("../src/filterByTerm"); // ...
Сохраните файл и запустите тест с покрытием:
npm test -- --coverage
Вот что вы получаете:
PASS __tests__/filterByTerm.spec.js Filter function ✓ it should filter by a search term (link) (3ms) ✓ it should filter by a search term (uRl) (1ms) ✓ it should throw when searchTerm is empty string (2ms) -----------------|----------|----------|----------|----------|-------------------| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | -----------------|----------|----------|----------|----------|-------------------| All files | 87.5 | 75 | 100 | 100 | | filterByTerm.js | 87.5 | 75 | 100 | 100 | 3 | -----------------|----------|----------|----------|----------|-------------------| Test Suites: 1 passed, 1 total Tests: 3 passed, 3 total
Хорошее резюме покрытия тестирования для нашей функции. Как вы можете видеть, строка 3 раскрыта. Попробуйте достичь 100% покрытия кода, протестировав новое утверждение, которое я добавил.
Если вы хотите, чтобы покрытие кода всегда было активным, настройте Jest в package.json следующим образом:
"scripts": { "test": "jest" }, "jest": { "collectCoverage": true },
Вы также можете передать флаг тестовому скрипту:
"scripts": { "test": "jest --coverage" },
Если вы хотите это визуализировать, есть также способ получить отчет в формате HTML для покрытия кода, просто настройте Jest следующим образом:
"scripts": { "test": "jest" }, "jest": { "collectCoverage": true, "coverageReporters": ["html"] },
Теперь каждый раз, когда вы запускаете тест npm, вы можете получить доступ к новой папке с именем coverage в папке вашего проекта: getting-started-with-jest/coverage/. Внутри этой папки вы найдете несколько файлов, где /coverage/index.html — это полная сводка HTML покрытия вашего кода:
Если вы нажмете на имя функции, вы также увидите точную непроверенную строку кода:
Аккуратно? С покрытием кода вы можете узнать, что тестировать, если сомневаетесь.
React — очень популярная библиотека JavaScript для создания динамических пользовательских интерфейсов. Jest отлично работает для тестирования приложений React (Jest и React от инженеров Facebook). Jest также является test runner по умолчанию в Create React App.
Если вы хотите узнать, как тестировать компоненты React, ознакомьтесь с разделом Testing React Components: The Mostly Definitive Guide. Руководство охватывает компоненты модульного тестирования, компоненты класса, функциональные компоненты с хуками и новый Act API.
Тестирование — это большая и увлекательная тема. Существует множество типов тестов и множество библиотек для тестирования. В этом руководстве Jest вы узнали, как настроить Jest для отчетов о покрытии, как организовать и написать простой модульный тест и как тестировать код JavaScript.
Чтобы узнать больше о UI тестировании, я настоятельно рекомендую взглянуть на JavaScript End to End Testing with Cypress.
Даже если это не связано с JavaScript, я также предлагаю прочитать Test-Driven Development with Python от Гарри Персиваля. Он полон советов и подсказок по всем вопросам тестирования и подробно описывает все виды тестов.
Если вы готовы сделать скачок и узнать об автоматизированном тестировании и непрерывной интеграции, то Automated Testing and Continuous Integration in JavaScript для вас.
Вы можете найти код для этого урока на Github: getting-started-with-jest, а также решение для упражнений.
Спасибо за чтение и следите за обновлениями!
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…
View Comments
Спасибо, отличное объяснение для новичков
было круто!