Pull to refresh

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

IT systems testing *JavaScript *Programming *Web services testing *TypeScript *
Recovery mode

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


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


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


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


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


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


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


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


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


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


Тесты должны соответствовать шаблону 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) даже услужливо остановит отладчик в этом месте, позволяя вам сразу же приступить к исследованию проблемы.


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

Tags:
Hubs:
Total votes 23: ↑16 and ↓7 +9
Views 9.6K
Comments 11
Comments Comments 11