Jest и фермент для модульного тестирования реакции

React.js

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

написать впереди

Прежде чем писать модульные тестовые примеры, нам нужно понять причину написания тестовых случаев.Целью написания тестовых случаев является обеспечение безопасности итерации кода, а не 100% покрытие или прохождение кейса, покрытие и кейс предназначены только для реализации коэффициентов безопасности кода.

модульный тест(Модульное тестирование): В прошлом модульное тестирование внешнего интерфейса могло быть относительно незнакомой работой, но после разработки внешнего интерфейса за последние несколько лет наши требования к надежности кода постепенно улучшились, в результате чего стало больше бизнес-логика.В то же время, как ближайшей к пользователю части всего звена, цена поломки и блокировки системы очень высока. Если вы используете SSR, более фатально отображать ошибку непосредственно на сервере.

Интерфейсное модульное тестирование может в определенной степени гарантировать:

  • Гарантировать качество кода, отправляемого каждый раз в ходе итеративного процесса;
  • Целостность оригинального функционала при рефакторинге кода;
  • Побочные эффекты каждой итерации кода можно контролировать;

По сравнению с внутренним кодом внешний код включает в себя больше контента, связанного с DOM Как протестировать неструктурированный контент?

airbnb предоставляет более подходящее решение для модульного тестирования React, которое в сочетании с Jest и husky может гарантировать, что код каждого коммита соответствует спецификации, а код в покрытии полностью функционален.

UT в библиотеку

Требования к библиотеке для модульного тестирования очень высоки. Поскольку библиотека может быть представлена ​​несколькими бизнес-направлениями и проектами,Как только возникает какая-либо проблема с этой библиотекой, масштаб воздействия очень велик.. Мы не можем просить QA вернуться к нескольким направлениям бизнеса (боюсь, они убьют нас и принесут в жертву небу).

Чтобы гарантировать, что итерация lib не повлияет на исходные бизнес-функции, очень хорошим методом является модульное тестирование. Поскольку наш основной стек технологий по-прежнему основан на различных решениях React, существует множество бизнес-компонентов и общих компонентов, которые используются несколькими бизнес-направлениями. Компонентный проект архитектуры lerna будет запускать UT каждый раз, когда делается фиксация для выполнения функциональной регрессии.

UT в бизнесе

Бизнес-код, как правило, не имеет такого высокого спроса на модульное тестирование, как библиотека, но доступ к UT в некоторой основной бизнес-логике также может обеспечить общее качество кода. По крайней мере, это может гарантировать, что бизнес-код не сообщает об ошибках во время обычного процесса рендеринга.

Рамка

Я кратко описал важность модульного тестирования для внешнего кода.Многие говорят, что нынешний круг фронтенда такой же, как круг развлечений.Действительно, всегда есть много необязательных тестовых фреймворков.После жасмина и мокко я пришел шутить.

TL;DR

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

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

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

стек технологий

Наконец, мы интегрировали следующие библиотеки для модульного тестирования React в различных сценариях:

  • Jest: среда модульного тестирования
  • энзим: библиотека тестирования React
  • Nok: имитация асинхронного запроса
  • Async-wait-until: уведомление об окончании асинхронной операции
  • Husky: выполнение модульных тестов на этапе предварительной фиксации

настроить

Jest

Сам Jest известен своей простотой настройки, а энзим представляет собой библиотеку для тестирования plug-and-play. Таким образом, процесс настройки относительно прост.

module.exports = {
    // 单元测试环境根目录
    rootDir: path.resolve(__dirname),
    // 指定需要进行单元测试的文件匹配规则
    testMatch: [
        '<rootDir>/test/**/__test__/*.js'
    ],
    // 需要忽略的文件匹配规则
    testPathIgnorePatterns: [
        '/node/modules'
    ],
    testURL: 'http://localhost/',
    // 是否收集测试覆盖率,以及覆盖率文件路径
    collectCoverage: true,
    coverageDirectory: './coverage'
};

Выше приведены некоторые из наиболее важных элементов конфигурации. Большинство из них относительно легко понять, иtestURLЭтот элемент конфигурации необходимо объяснить,Это правило представляет собой URL-адрес, по которому выполняется текущий тестовый пример., хотя мы не можем видеть полную страницу во время теста, сам тест-кейс монтируется на страницу, и URL этой страницы проходит черезtestURLУказано.

В этой конфигурации Jest во всех тестовых случаях, если выполнитьlocation.hrefполучитеhttp://localhost/Этот URL-адрес, этот элемент конфигурации имеет решающее значение в тех случаях, когда требуются сетевые запросы.

При выполнении вы можете указать путь к файлу конфигурации Jest:

~ jest --config ./scripts/jest.config.js

Если путь к файлу не указан, по умолчанию берется файл конфигурации из текущего пути к файлу.

enzyme

Сам Enzyme не нужно настраивать, как plug-and-play библиотеку для тестирования React можно рассматривать как позволяющую нам избавиться от внешнего интерфейса.инженер-конфигураторморе горечи.

А вот для разработки на базе React нужно установить соответствующий React Adapter, например, если нужно использоватьstatic getDerivedStateFromPropsметод, то необходимо ввестиenzyme-adapter-react-16библиотеку, чтобы убедиться, что версия, отображаемая ферментом, совпадает с версией, которую вы используете.

В процессе УТ Jest сначала проверит настроен ли проект.babelrcфайл, если он настроен, он автоматически отредактирует babel на основе этого файла, а затем выполнит тестовый пример.

Зависимости созданной вручную демонстрационной среды:

  "dependencies": {
    "react": "^16.7.0",
    "react-dom": "^16.7.0"
  },
  "devDependencies": {
    "babel-plugin-transform-async-to-generator": "^6.24.1",
    "babel-plugin-transform-class-properties": "^6.24.1",
    "babel-preset-env": "^1.7.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "babel-preset-stage-0": "^6.24.1",
    "babel-preset-stage-3": "^6.24.1",
    "enzyme-adapter-react-16": "^1.7.1",
    "enzyme": "^3.8.0",
    "jest": "^23.6.0"
  },
  "scripts": {
    "test": "jest --config ./jest.config.js"
  }
// ./__test__/index.js
import Test from '../src';
import Enzyme, { shallow, render, mount } from 'enzyme';
import React from 'react';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({
    adapter: new Adapter()
});

И адаптер фермента нужно инициализировать, черезEnzyme.configureЗадает импортируемый экземпляр адаптера.

Это завершает среду Enzyme + React + Jest.

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

утверждение

В настоящее время утверждения различных тестовых фреймворков начали сходиться, и синтаксис утверждений, используемый Jest, похож на синтаксис mocha, который мы использовали ранее.

Можно использовать набор тестовdescribeЧтобы описать, набор тестов может содержать несколько случаев для проверки результатов рендеринга компонентов в различных сценариях.

Начнем с очень простого компонента React:

import React from 'react';

export default class Text extends React.Component {
    render() {
        return (<div className="test-container" />)
    };
}

Для этого компонента нам нужно определить, успешно ли отрисован элемент div, и имя класса элементаtest-container.

Вот минимальная версия дела:

describe('test suite: Test component', () => {
    it('case: expect Test render a div with className: test-container', () => {
        const wrapper = shallow(<Test />);

        expect(wrapper.find('.test-container').length).toEqual(1);
    });
});

воплощать в жизньnpm run test, можно получить следующие результаты:

测试1

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

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

Тестовые случаи также могут быть сложными

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

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

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

история и Date.now()

В бизнес-коде нам много раз нужно перейти на страницу или изменить хэш. все дляlocationоперации придутся наwindow.locationна объекте.

энзим фактически строит для нас виртуальную среду DOM, мы можем получить соответствующие элементы DOM иwindow,documentобъект для выполнения манипуляций с DOM.

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

Посмотрите на следующий компонент:

class Time extends React.Component {
    static propTypes = {
        time: PropTypes.number
    };

    constructor(props) {
        super(props);
        this.state = {
            before: Date.now() < props.time
        }
    }
    
    render() {
        const { before } = this.state;
        const { time } = this.props;
        
        if (before) {
            return (
                <div className="before">
                    {`now is before time: ${time}`}
                </div>
            );
        } else {
            return (
                <div className="after">
                    {`now is after time: ${time}`}
                </div>
            );
        }
    }
}

При написании модульных тестов мы обнаружим, что из-за несоответствия текущего времени, какpropsВремя прибытия в иDate.now()Были сопоставлены, результаты противоречивы, что может привести к неконтролируемым результатам тестового примера.

чтобы убедиться, чтоDate.now()Полученное значение совпадает, нам нужно переписать значение на DOMDateобъект.

describe('test suite: Time component', () => {
    const NOW_TO_CACHE = global.Date.now;
    const NOW_TO_USE = jest.fn(() => 1547717952668);

    beforeEach(() => {
        global.Date.now = NOW_TO_USE;
    });
    afterEach(() => {
        global.Date.now = NOW_TO_CACHE;
    });
    it('case: now is less than props\' time', () => {
        const wrapper = shallow(<Time time={1547717952669} />);

        console.log(Date.now())

        expect(wrapper.find('.before').length).toEqual(1);
    });

    it('case: now is greater than props\' time', () => {
        const wrapper = shallow(<Time time={1547717952667} />);

        console.log(Date.now())

        expect(wrapper.find('.after').length).toEqual(1);
    })
});

beforeEachа такжеafterEachДва хука будут выполняться отдельно до или после выполнения каждого кейса.Перед каждым кейсом будет выполнено выполнение.global.Date.nowпереписать, затем после окончания дела заменитьglobal.Date.nowВернитесь к исходному методу.

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

  • Сколько раз функция выполнялась
  • Параметры каждый раз, когда функция выполняется
  • или даже каждый раз, когда функция вызываетсяthisнаправление

Date.now

Видно, что для всехDate.now()Метод, полученное текущее время копируется в определенное число, чтобы обеспечить независимость тестовых случаев от времени.

дляhistory,Date.nowЭтот тип крепится кwindowилиdocumentВышеупомянутые объекты экземпляра, мы все можем передатьjest.fnЧтобы перезаписать его методы, обеспечить порядок вызова этих методов и правильность результатов вызова, мы также можемjest.fnУтверждение делается внутри, чтобы определить, возникает ли ошибка во время каждого выполнения.

запрос на получение

Внешний интерфейс — это представление, и в некоторых сценариях он опирается на модель, предоставленную серверной частью для рендеринга.Правильность API часто напрямую влияет на правильность результата рендеринга всей страницы.

И в некоторых сценариях некоторый код может быть вPromiseодеялоresolveбудет вызван после этого.

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

Здесь нужно использовать:

Nock: библиотека насмешек и ожиданий HTTP-сервера для Node.js

Async-wait-until: подождите, пока завершится предикат, и разрешите обещание

эти две библиотеки.

Во-первых, обратите внимание на следующий компонент:

import React from 'react';
import fetch from 'isomorphic-fetch';

export default class AsyncComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            user: {}
        }
    }

    componentDidMount() {
        this.fetchUser()
            .then(res => {
                this.setState({user: res});
            });
    }

    fetchUser = () => {
        return fetch(`${location.origin}/api/user/get`, {
            method: 'GET'
        }).then(ret => {
            return ret.json();
        }).catch(err => {
            console.error(err);
        });
    }

    render() {
        const { user } = this.state;
        return (
            <div className="user-profile">
                <p className="name">{user.name}</p>
                <p className="age">{user.age}</p>
            </div>
        );
    }
}

внутри компонентаcomponentDidMountЭтап отправляет запрос на получение данных, когда клиент выполняет рендеринг, и заполняет их на странице.

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

Вопросы и ответы:

Вопрос: Первый: Как протестировать обратный вызов сетевых запросов?

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

А: Нок

В: Во-вторых: сетевой запрос асинхронный, что если написать асинхронные тестовые примеры?

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

A: асинхронно-подождите, пока

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

import Async from '../src/async';
import Enzyme, { shallow, render, mount } from 'enzyme';
import React from 'react';
import Adapter from 'enzyme-adapter-react-16';
import nock from 'nock';
import waitUntil from 'async-wait-until';

Enzyme.configure({
    adapter: new Adapter()
});

describe('test suite: Async component', () => {
    beforeAll(() => {
        nock('http://localhost/api/user')
            .get('/get')
            .reply(200, {
                "name": "lucas",
                "age": 20
            });
    });

    afterAll(() => {
        nock.cleanAll();
    });

    it('case: expect component did mount will trigger re-render', async () => {
        const wrapper = mount(<Async />);

        await waitUntil(() => wrapper.state('user').name === 'lucas');

        expect(wrapper.find('.name').text()).toBe('lucas');
        expect(wrapper.find('.age').text()).toBe('20');
    });
});

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

Во-первых, мы добавили два хука в этот набор тестов,beforeAllбудет выполнен один раз перед выполнением всех дел в этом наборе, иafterAllОн будет выполнен один раз после выполнения всех дел.

beforeAll, мы смоделировали результат запроса на выборку в компоненте через nock и дали резолв-ответ.

Когда React выполняется дляcomponentDidMountКогда запрос на выборку сделан, запрос будет вызван в ноке. Обратите внимание, что наш URL-адрес для полученияhttp://localhost/api/user/get, о котором упоминалось ранее, устанавливается в конфигурационном элементе JesttestURLэффект.testURLУказанный URL будет использоваться в качестве тестовой страницыlocation.origin.

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

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

После завершения этого пакета логика кода перейдет кafterAllв крючок. Это называется здесьnock.cleanAll(), который используется для очистки интерфейса предыдущего макета, то есть для указания того, что область действия этого макета находится только в текущем наборе.

В это время мы снова бежимnpm run test, можно получить следующие результаты испытаний:

test async

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

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

Чтобы протестировать ситуацию с отказом, нам нужен новый набор, в этом наборе мы имитируем интерфейс ответа на отказ:

describe('test suite: Async component', () => {
    let resolve = false;
    beforeAll(() => {
        nock('http://localhost/api/user')
            .get('/get')
            .reply(400, () => {
                resolve = true;
            });
    });

    afterAll(() => {
        nock.cleanAll();
    });

    it('case: expect component fetch error will not block rendering', async () => {
        const wrapper = mount(<Async />);

        await waitUntil(() => resolve);

        expect(wrapper.find('.name').text()).toBe('');
        expect(wrapper.find('.age').text()).toBe('');
    });
});

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

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

async error test

Интерактивное моделирование

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

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

Рассмотрим следующий компонент:

import React from 'react';
import fetch from 'isomorphic-fetch';

export default class Text extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            value: ''
        };
    }

    onInputChanged = (e) => {
        this.setState({
            value: e.target.value
        });
    }

    onClicked = () => {
        const { value } = this.state;
        this.postValue(value)
            .then(res => {
                this.setState({
                    value: ''
                });
            });
    }

    postValue = (value) => {
        return fetch(`${location.origin}/api/value`, {
            method: 'POST',
            body: JSON.stringify({value}),
        }).then(ret => {
            return ret.json();
        });
    }

    render() {
        const { value } = this.state;
        return (
            <div className="form">
                <input value={value} onChange={this.onInputChanged} />
                <button className="submit" onClick={this.onClicked}>提交</button>
            </div>
        )
    }
}

Это обычное поле ввода React, мы будем вводить полеvalueсвязываются сstateнад. Ожидается, что состояние компонента может быть изменено посредством пользовательского ввода. Когда пользователь нажимает «Отправить», значение может быть получено со страницы и отправлено на сервер. После получения правильного обратного вызова содержимое поля ввода очищается. .

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

Учтите несколько важных моментов:

  1. Инициировать событие onchange поля ввода
  2. Дождитесь окончания события ввода поля ввода
  3. Событие нажатия кнопки триггера
  4. принести
  5. дождитесь окончания выборки
  6. Очистите входное содержимое в обратном вызове

фермент предоставляет некоторые методы запуска событий. когда мы используемmountПри монтировании компонента в виртуальный DOM вы можете передатьwrapper.simulate()Метод для запуска различных событий DOM.

Сначала проверьте правильность рендеринга компонента:

it('case: expect input & click operation correct', async () => {
    const wrapper = mount(<Interaction />);

    const input = wrapper.find('input').at(0);
    const button = wrapper.find('button').at(0);

    expect(input.exists());
    expect(button.exists());
});

Затем вам нужно вызвать событие onchange ввода, чтобы изменить текущее состояние:

input.simulate('change', {
    target: {
        value: 'lucas'
    }
});

expect(wrapper.state('value')).toBe('lucas');

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

button.simulate('click');

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

Вот готовый набор тестов:

import Interaction from '../src/interaction';
import Enzyme, { shallow, render, mount } from 'enzyme';
import React from 'react';
import Adapter from 'enzyme-adapter-react-16';
import nock from 'nock';
import waitUntil from 'async-wait-until';

Enzyme.configure({
    adapter: new Adapter()
});

describe('test suite: Async component', () => {
    let resolve = false;
    beforeAll(() => {
        nock('http://localhost/api')
            .post('/value')
            .reply(200, () => {
                resolve = true;
                return {};
            });
    });

    afterAll(() => {
        nock.cleanAll();
    });

    it('case: expect input & click operation correct', async () => {
        const wrapper = mount(<Interaction />);

        const input = wrapper.find('input').at(0);
        const button = wrapper.find('button').at(0);

        expect(input.exists());
        expect(button.exists());

        input.simulate('change', {
            target: {
                value: 'lucas'
            }
        });

        expect(wrapper.state('value')).toBe('lucas');

        button.simulate('click');

        await waitUntil(() => resolve);

        expect(wrapper.state('value')).toBe('')
    });
});

Завершите весь тестовый проход, и покрытие будет 100%

interaction test

наконец

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

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

Наконец, при написании тестовых случаев вы должны помнить, что юнит-тестирование — это не стекирование покрытия, а обеспечение охвата каждой функциональной детали.