Как стать автором
Обновить

Комментарии 13

Молодцы. Спасибо за развёрнутый материал.


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


Думаю, если мне теперь когда нибудь понадобится react.js, то Ваша статья весьма пригодиться.

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


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


$my_project $mol_page
    title @ \Active projects
    body /
        <= Active $mol_grid
            records <= projects /
            selected
    foot /
        <= Info $mol_button_minor
            title <= info_title @ \Project information
            event_click?val <=> event_info?val null
        <= Create $mol_button_minor
            title <= create_title @ \Create project
            event_click?val <=> event_create?val null

Визуализация компонента


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


Сначала проверка на наличие и расположение элементов:


Ваш код


// если тест провалится - в стектрейсе будет имя теста, а не "anonymous function"
'Check structure'() {

    // Никаких фабрик, монтирований и прочего непотребства
    const view = new $my_projects

    // индикатор загрузки не нужно проверять в каждом компоненте
    // $mol_assert_ok( view.Progress() )

    // Активные проекты выводятся в теле страницы
    // Все вложенные блоки доступны через одноимённые методы компонента
    // Не нужно искать вложенные блоки через хрупкие css-селекторы.
    $mol_assert_equal( view.Body().sub()[0] , view.Active() )

    // В подвале - две кнопки с говорящими именами
    // TypeScript нам если что подскажет правильные имена
    $mol_assert_equal( view.Foot().sub()[0] , view.Info() )
    $mol_assert_equal( view.Foot().sub()[1] , view.Create() )

}

Далее проверим параметры вложенных элементов:


Ваш код


'Sub components parameters'() {

    const view = new $my_projects

    // По умолчанию там пустой массив, так что даже мочить не надо - проверяем, что передано именно он
    $mol_assert_equal( view.Active().records() , view.projects() )

    // Не нужно зубрить огромный bdd-like api, просто используем ассерты везде
    $mol_assert_equal( view.Active().selected() , view.selected()  )

    // Не нужно вешать лишние обработчики, ведь коммуникация через selected - двусторонняя
    // $mol_assert_ok( typeof view.Active().event_select )

    // Тексты локализованные, но нам важно их равенство, а не конкретное значение
    $mol_assert_equal( view.Info().title() , view.info_title() )
    $mol_assert_equal( view.Create().title() , view.create_title() )
}

Ну и ещё провязку событий проверим:


Ваш код


'Events binding'() {

    const view = new $my_projects

    // Замочили обработчик создания проекта
    // Можно было бы и через sinon.spy, но зачем? Кода меньше не станет, зато тут явно видно, как проверяется
    view.event_create = event => { ++ click_count }
    let click_count = 0

    // Кликнули по кнопке "Создать проект"
    view.Create().event_activate( new MouseEvent( 'click' ) )

    // Проверили число вызовов обработчика
    $mol_assert_equal( click_count , 1 )
}

Итого:


  • Уникальные имена всех вложенных блоков позволяют писать простые, лаконичные и крепкие тесты.
  • Вовсе не нужно тащить в проект 100500 библиотек для тестирования, и без них можно писать компактные и ясные тесты.
  • Компоненты-классы гораздно удобнее в тестировании, чем компоненты-функции.
  • Тестировать лучше нетривиальную логику, иначе тесты будут часто ломаться и требовать слишком много времени на свою поддержку.
Так и хочется вставить ту самую картинку с Питером Гриффином, сидящим за партой.

Некогда смотреть по сторонам, да?

А можно узнать, насколько затратными и насколько статистически качественными у вас получаются тесты? Сколько строк тестов приходится на одну строку тестируемого кода и сколько реальных ошибок в среднем успевает отловить тест, будучи неизменным после того, как компонент полностью реализован?
Хороший вопрос, к сожалению, не могу конструктивно ответить на него, так как не проводились замеры подобных метрик. Но это хорошая идея сделать это в следующий раз и на основе этих замеров понять, что и вправду эффективно, а что нет.

Если не вдаваться в цифры, то тесты написанные на редукторы, сервисы и действия достаточно статичны. Реже менялись компонентные тесты (в нашем случае приложение не большое и требования не сильно прыгали, я допускаю, что на некоторых проектах скелеты составных компонентов могут по 100 раз переписываться). Наименее статичны были e2e тесты.

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

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

хорошая статья, к некоторым вещам которые здесь описаны пришел сам со временем, к примеру описывать 2 экспорта для компонента и HOC-обертки.
у меня тоже сейчас для юнит тестов mocha+chai+sinon + enzyme, чем больше пользуюсь mocha тем больше скучаю по жасмину с его нормальным синтаксисом и нормальными spy.

пара вопросов:
1) если вы используете protractor то вопрос зачем использовать mocha если можно заменить зоопарк (mocha+chai+sinon) на jasmine и использовать одну либу для e2e и юнит тестов?
кстати большую часть интеграционных тестов можно писать тут же при использовании `enzyme mount`

2) как вы тестируете third-party компоненты которые в render возвращают null или пустой div, а на componentDidMount
вставляют большой кусок HTML с логикой.
в таких компонентах
mount(<ThirdPartyComponent {...props} />).debug()
— покажет только верхний div. и поиск
(wrapper.find('.some_element_inside')) 
не найдет этого элемента…
хотя при
mount(<ThirdPartyComponent {...props} />).html()
— покажет полный HTML который компонент должен был отрендерить.
я на данный момент обхожу это так:

import cheerio;

const wrapper = mount(<ThirdPartyComponent {...props} />);
const $ = cheerio.load(wrapper.html());
$('.some_element_inside').length  // will find element.


может у вас есть идеи как с этим еще можно работать?

Попробуйте expect. Это набор матчеров и мок-функций в стиле Jasmine.

я им и пользуюсь, но.он.меня.бесит.многословностью.и.точками(«впрочем тут уже дело привычки»)

Вы говорите про chai. А я говорю про expect. Там обычные джасминовские


expect(myValue).toEqual(123);

var spy = expect.createSpy()
spy();

expect(spy).toHaveBeenCalled();

Все как в Jasmine только без Jasmine.

хм, вы мне раскрыли глаза, я почему-то думал что это chai-expect

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

… В jasmine при включенном автокомплите camelCase быстрее получится найти метод чем писать 3 метода по отдельности...
Спасибо за комментарий

1) Как сказал бы один из наших разработчиков (Макс): «Так исторически сложилось»
Если серьезно, то е2е тесты появились позже, чем остальные. И вы абсолютно правы, если бы мы стартовали все одновременно, то одного jasmine нам было бы достаточно. Однако тогда мы сделали ставку на mocha.

Мы могли бы взять mocha в качестве фреймворка для protractor, но такая связка, как написано на офф сайте: «Limited support», jasmine же фреймворк по умолчанию и он же лучше всего поддерживается командой protractor. Я сомневаюсь, что мы встретили бы какие-то проблемы с mocha, но все же решили использовать jasmine. И как вы верно заметили, сейчас есть конфуз, 2 BDD фреймворка в проекте.

2) Очень интересный вопрос. Возможно кто-то из мимо проходящих хабровчан сможет помочь. К сожалению, мне не чего ответить, так как подобных компонентов у нас не было, где бы render возвращал null.
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.