- PVSM.RU - https://www.pvsm.ru -

Заблуждения об автоматическом тестировании

Здравствуйте, меня зовут Дмитрий Карловский и это продолжение традиционной рубрики "Почему мы так не любим писать тесты?". Короткий ответ: потому, что получаемые от них бонусы не перевешивают затрачиваемых усилий. Если это так, значит мы делаем что-то не правильно. Давайте разберёмся что же могло пойти не так..

Картинка для привлечения внимания

Данная заметка выросла из главы "Заблуждения" лонгрида "Концепции автоматического тестирования" [1], посредством дополнения новыми заблужениями и аргументами.

Модульные тесты быстрее компонентных

Да, моки как правило исполняются быстрее, чем реальный код. Однако они прячут некоторые виды ошибок, из-за чего приходится писать больше тестов. Если фреймворк не умеет в ленивость и делает много лишней работы для поднятия дерева компонент (как, например, web-components [2] гвоздями прибитые к DOM или TestBed в Angular [3] создающий всё на свете при инициализации), то тесты существенно замедляются, но не так чтобы фатально. Если же фреймворк не рендерит, пока его об этом не попросят и не создаёт компоненты, пока они не потребуются (как, например, $mol_view [4]), компонентные тесты проходят не медленнеее модульных.

С компонентными тестами сложно локализовать ошибку

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

Однако, исполнять компонентные тесты имеет смысл в порядке от менее зависимых компонент к более зависимым. Тогда первый же упавший тест покажет на источник проблемы. Остальные тесты обычно можно уже и не исполнять, что здорово экономит время прохождения тестов. Опять же, в MAM архитектуре [5] весь код (что продакшен, что тестовый) сериализуется в едином подярке. Это гарантирует, что тесты зависимости будут исполнены до тестов зависимого, а значит тот может смело полагаться на то, что зависимость работает корректно. Если вы используете иные инструменты — подумайте, как с их помощью можно выстраивать тесты в правильном порядке.

Шаблоны тестировать не надо

Тестировать надо логику. Редкий шаблонизатор (mustache [6], view.tree [7]) запрещает встраивать логику в шаблоны, а значит их тоже надо тестировать. Часто модульные тесты для этого не годятся (enzyme [8] в качестве редкого исключения), так что всё равно приходится прибегать к компонентным.

Тесты должны соответствовать шаблону Given/When/Then

Да, иногда в тестовом сценари можно выделить эти шаги, но не стоит высасывать их из пальца, когда их нет. Зачастую сценарий имеет более простую (например, только Then блок) или сложную (Given/Check/When/Then) структуру. Несколько примеров:

Чистые функции часто имеют только блок Then:

console.assert( Math.pow( 2 , 3 ) === 8 ) // Then

Не менее часто действие (When) заключается именно в подготовке состояния (Given):

component.setState({ name : 'Jin' }) // Given/When
console.assert( component.greeting === 'Hello, Jin!' ) // Then

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

ensurePerson({ name : 'Jin' , age : 33 })

Подобный же код совершенно бессмысленный:

const component = new MyComponent // Given
expect( component ).toBeTruthy() // Then

Так же как тест, который никогда не падал — ничего не тестирует. Так и ассерт, который никогда не кидал исключение — ничего не проверяет.

В правильном тесте должен быть только один assert

Не редко необходимо проверять правильно ли мы выполнили подготовку состояния поверкой в середине:

wizard.nextStep().nextStep() // Given
console.assert( wizard.passport.isVisible === false ) // Check

wizard.toggleRegistration() // When
console.assert( wizard.passport.isVisible === true ) // Then

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

wizard.nextStep().nextStep() // When
console.assert( wizard.passport.isVisible === false ) // Then

wizard.nextStep().nextStep() // Given
wizard.toggleRegistration() // When
console.assert( wizard.passport.isVisible === true ) // Then

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

wizard.nextStep().nextStep() // When
console.assert( wizard.passport.isVisible === true ) // Then

Теперь, если toggleRegistration реализован так, что, например, использует своё состояние для ускорения работы, то он будет проходить второй тест, по прежнему возвращая true и получится, что первое применение toggleRegistration не будет ничего менять в форме:

isPassportVisible = false
toggleRegistration() {
     this.passport.isVisible = this.isPassportVisible = !this.isPassportVisible
}

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

wizard.nextStep().nextStep() // When
console.assert( wizard.passport.isVisible === false ) // Then

wizard.toggleRegistration() // When
console.assert( wizard.passport.isVisible === true ) // Then

wizard.toggleRegistration() // When
console.assert( wizard.passport.isVisible === false ) // Then

Обычно аргументом против такого подхода выступает сложность понимания какой из ассертов упал. Но постойте, никто же не заставляет вас использовать такой инструмент тестирования, который не даёт исчерпывающей информации о месте падения теста. Хороший же инструмент (например, $mol_test [9]) даже услужливо остановит отладчик в этом месте, позволяя вам сразу же приступить к исследованию проблемы.

Подводя итог, можно порекомендовать писать тесты не по шаблону "Given/When/Then", а как небольшое приключение, стартующее из абсолютной пустоты и посредством некоторого количества действий, проходящее через некоторое количество состояний, которые мы и проверяем.

Автор: vintage

Источник [10]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/277351

Ссылки в тексте:

[1] "Концепции автоматического тестирования": https://habrahabr.ru/post/351430/

[2] web-components: https://www.webcomponents.org/

[3] TestBed в Angular: https://angular.io/guide/testing#component-with-a-dependency

[4] $mol_view: https://github.com/eigenmethod/mol/tree/master/view

[5] MAM архитектуре: https://github.com/eigenmethod/mam

[6] mustache: https://mustache.github.io/

[7] view.tree: https://github.com/eigenmethod/mol/tree/master/view#viewtree

[8] enzyme: http://airbnb.io/enzyme/

[9] $mol_test: https://github.com/eigenmethod/mol/tree/master/test

[10] Источник: https://habrahabr.ru/post/353080/?utm_campaign=353080