Тестирование для “чайников”.

Andriy Ivashchenko
10 min readJun 14, 2018

--

Что такое тестирование

Тестирование — это процесс в программировании, позволяющий проверить на корректность отдельные модули исходного кода программы.

Когда нужно и не нужно тестировать

Всегда ли нужно тестировать ваше приложение? Рекомендуем ответить вам самим на этот вопрос после следующего утверждения: “Любой долгосрочный проект без надлежащего покрытия тестами обречен рано или поздно быть переписанным с нуля”.

Однако существуют ситуации, когда необходимость в написании тестов не столь очевидна. Скорее всего тесты не нужны вам если:

  • Вы всегда пишете код для себя, создавая pet проект, не имея планов на его дальнейшее коммерческое использование или распространение в рамках сообщества.
  • Вы создаете рекламные одностраничники, продающие страницы, простые флеш-играми или баннеры — сложная верстка/анимация или большой объем статики. Никакой логики нет, только представление.
  • Создание статического сайта-визитки, т.е. 1–4 html-страницы с одной или несколькими формами для отправки данных. Тут закономерно нет никакой особенной логики, быстрее просто все проверить самостоятельно, так сказать «руками».
  • Вы делаете рекламный проект для выставки. Срок работы — от нескольких недель до месяца. В начале проекта не до конца известно, что именно должно получиться в конце. Задача проекта — отработать несколько дней на выставке в качестве презентации.

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

Основные термины (пирамида, TDD, BDD, stubs, mock)

Существует несколько подходов к написанию тестов. Первая модель — классика: сначала разработка, а затем тестирование “code first”. Это означает, что сначала происходит написание кода, затем мы тестируем продукт и отправляем его или на доработку, или переходим к следующей стадии разработки.

Другой подход можно назвать “test first” режимом. Это означает, что мы можем начать тестирование еще до написания самой функции — например, мы можем создать единичный тест или автоматически выполняемый набор тестов до того, как функция или какой-то кусок кода будет разработан и внедрен в приложение. Одним из наиболее популярных примеров здесь является Test-Driven Development.

Тем не менее, стоит упомянуть, что техника “test first” не так популярна, как “code first”. Это связано с тем, что в большинстве проектов все еще сложно автоматизировать что то, что еще не было разработано. Обобщая оба упомянутых выше подхода, можно сделать вывод, что нет особой разницы и что автоматизацию тестов мы можем использовать в любом из вариантов. Ни один из этих подходов не может считаться хорошим или плохим и выбор в первую очередь зависит от проекта т.е. каждый конкретный случай следует рассматривать отдельно.

Также наряду с термином ТDD вы можете услышать и о BDD подходе.

https://qph.ec.quoracdn.net/main-qimg-341f02eb4b8f2095629aae0949f9eef2

В основе Test-driven development (TDD) лежит 5 основных этапов:

  1. Сначала разработчик пишет несколько тестов.
  2. Затем разработчик запускает эти тесты и (очевидно) они терпят неудачу, потому что ни одна из этих функций еще не реализована.
  3. Далее разработчик действительно реализует эти тесты в коде.
  4. Если разработчик хорошо пишет свой код, то на следующем этапе он увидит, что тесты проходят.
  5. Разработчик может затем реорганизовать свой код, добавить комментарии так как он уверен, что если новый код что-то сломает, тогда тесты предупредят об этом.
test('равно 1 для массива с одним элементом', function (){    assert.equal(1,[1].length);});

Behavior-driven development (BDD) — подход создан для того, чтобы исправить проблемы, которые могут возникнуть при использовании ТDD, а именно, обеспечить лучшее взаимопонимание внутри команды, т.е. не только для разработчиков, облегчить поддержку кода через наглядное представление о его функциональности, тесты и их результаты выглядят более “человечно”, облегчается процесс миграции при переходе на другой язык программирования.

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

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

it('должно вернуть 1, когда передан массив с одним элементов', function (){   [1].length.should.equal(1);});

Еще одной важной концепцией тестирования является тестовая пирамида. Пирамида тестирования используется для распределения тестов по уровням приложения.

https://www.sealights.io/wp-content/uploads/2017/01/automatedtestingpyramid.png

Каждое приложение можно разделить на несколько слоев. Рассмотрим типичное расслоение с уровнем компонентов, сервисами и пользовательским интерфейсом. Нижняя часть пирамиды покрыта модульными (unit) тестами. Они написаны в основном разработчиками и охватывают атомарные компоненты, такие как классы, методы и функции. Запускаются очень часто, работают быстро и их количество в рамках приложения велико.

Б. Страустрап в своей книге о C++ предлагает следующий подход к разделению кода на отдельные блоки: если «это» действие — сделайте метод. Если несколько действий объединены общим смыслом и/или процессом — объявите класс. Если придерживаться этого правила, то автоматически класс будет модулем вашего приложения.

Следующий уровень — интеграционные тесты. Т.е. когда идет проверка, не ломает ли новый функционал код, который уже написан ранее в рамках системы. Также тут мы можем иметь сценарии, которые охватывают более сложные функции, такие как тесты API. Запускаем реже, как правило, при мердже веток или объединении больших участков кода.

В верхней части находятся тесты пользовательского интерфейса (end to end) Они действуют так же, как конечный пользователь работает с приложением. Запускаем очень редко — несколько раз за проект. Работают очень медленно.

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

Еще один набор терминов, с которыми придется столкнуться в процессе написания тестов — это стабы (stubs) и моки (mock).

Очень часто наш код (функция, модуль) имеют внешние зависимости. Внешняя зависимость — это все, что делает ваши тесты не правдивыми и сложно-поддерживаемыми. Файловая система — зависимость: структура каталогов может быть другой на другой машине. База данных — зависимость, ее может не быть на другой машине. Веб-сервис — зависимость: может не быть интернета или может присутствовать фаервол и.т.д

Если на вопрос: «будет ли этот компонент вести себя так же на другой машине?» вы отвечаете нет, то его необходимо “подменить” и тут вам на помощь как раз придут стабы и моки. Но есть и другая сторона медали, когда разработчик начинает увлекаться и приходит к тому, что подменяет вообще все. Соответственно тесты перестают проверять само приложение и начинают тестировать стабы, моки. Это в корне не верно. Если «живых» реализаций в тесте нет, то этот тест не тестирует ничего.

Иногда эти термины stubs и mock путают: разница в том, что стаб ничего не проверяет, а лишь имитирует заданное состояние. А мок — это объект, у которого есть ожидания. Например, что данный метод класса должен быть вызван определенное число раз. Иными словами, ваш тест никогда не сломается из-за «стаба», а вот из-за мока может.

https://res.cloudinary.com/practicaldev/image/fetch/s--hZmevHbU--/c_imagga_scale,f_auto,fl_progressive,h_500,q_auto,w_1000/https://thepracticaldev.s3.amazonaws.com/i/xh6hjugd2aghoxgtdy8r.png

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

Есть уже готовые фреймворки, которые предоставляют такой функционал: Sinon, Jasmine, enzyme, Jest, testdouble

Пример использования стаба (sinonjs.org)

it('resolves with the right name', done => {   const stub = sinon.stub(User.prototype, 'fetch')      .resolves({ name: 'David' })   User.fetch()      .then(user => {         expect(user.name).toBe('David')         done()      })})

Пишем тесты правильно (требования, оценка результата)

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

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

1) Тесты в пределах проекта должны быть расположены в соответствии с общей логикой и должны быть частью системы контроля версий. Например, если приложение монолитное, положите все тесты в папку test; если у вас много разных компонентов, храните тесты в папке каждого компонента.

2) Особое внимание уделите именованию тестов. Одна из лучших практик: добавьте к каждому проекту его собственный тестовый проект.

Если у вас, например, есть части системы your-project.models, your-project.controllers, то тесты для этих частей могут именоваться следующим образом: your-project.models.tests, your-project.controllers.tests

3)Такие же “логичные” походы используйте для именования тестовых классов или методов.

Например, один из вариантов именования для тестирования метода — <метод>_<cценарий>_<результат>.

Метод калькулятора суммирующий данные
Sum_2Plus5_Returned7

4)Каждый тестирующий класс или метод должен тестировать только одну сущность. Если процесс слишком сложен (например, покупка в интернет-магазине), разделите его на несколько частей и протестируйте их отдельно.

5)Не делайте ненужных утверждений (assertion). Какое конкретное поведение вы тестируете? Если это не основное поведение, то оно и не нуждается в тестировании! Помните, что модульные тесты — это спецификация дизайна того, как должно срабатывать определенное поведение, а не список наблюдений за всем кодом.

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

7)Не стоит писать велосипеды. Пользуйтесь готовыми тестовыми фреймворками. Подберите тот, который подходит вам в данном, конкретном случае.

8)Боритесь с зависимостями. Тесты не должны зависеть от окружения, в котором они выполняются. Например, не проверяйте настройки конфигурации устройства. По необходимости используйте стабы и моки, а также готовые фреймворки для их написания.

9)Не относитесь к своим тестам как к второсортному коду. Все принципы, применяемые в разработке продакшн-кода могут и должны применяться при написании тестов. (DRY, KISS)

10)Тест должен легко поддерживаться. Есть всего три причины, почему тест перестал проходить:

  • Ошибка в продакшн-коде: это баг, его нужно завести в баг-трекере и починить.
  • Баг в тесте: видимо, продакшн-код изменился, а тест написан с ошибкой (например, тестирует слишком много или не то, что было нужно).
  • Смена требований. Если требования изменились слишком сильно — тест должен упасть. Это правильно и нормально. Вам нужно разобраться с новыми требованиями и исправить тест. Или удалить, если он больше не актуален.

11)Тесты должны запускаться регулярно в автоматическом режиме.

Определить успешность вашей системы тестирования на проекте можно двумя способами:

  1. Количество багов в новых релизах (в т.ч. и регрессии).
  2. Покрытие кода.

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

Тестовое покрытие — полезный инструмент для поиска непроверенных частей кодовой базы. Тестовый охват мало полезен в качестве числового заявления о том, насколько хороши ваши тесты. “Нормальным” считается покрытие в пределах 80%.

Наиболее популярные инструменты для измерения покрытия кода, написанного на JavaScript istanbul.js blanket.js JSCover

Выбор фреймворка для создания тестов

На сегодняшний день доступна целая масса фреймворков для тестирования JavaScript-кода (overview).

Учитывая подобное разнообразие, выбор того или иного фреймворка, как правило, напрямую зависит от непосредственной задачи, которую мы ставим перед собой в процессе написания тестов. Идеально, когда функционал фреймворка покрывает несколько или все поставленные задачи (единая среда).

Например, поддерживаемая структура тестов. Если мы говорим о поддержке BDD, то следует выбирать среди Mocha, Jasmine, Jest, Сucumber.

Также доступны на выбор несколько assertion библиотек: Chai, Jasmine, Jest, Unexpected.

Если для вас важную роль играет представление и отображение результатов ваших проверок, то наибольший функционал в данной области предоставляют Mocha, Jasmine, Jest, Karma.

Для использования snap-shots в вашем тестировании следует обратить внимание на Jest или Ava.

Для борьбы с зависимостями и использования mocks и stubs следует обратить внимание на специализированные фреймворки типа Sinon.js, Jasmine, enzyme, Jest, testdouble.js.

Как уже упоминалось выше для измерения охвата и покрытия кода тестами возможно использование Istanbul, Jest.

Для функциональных тестов, для создания пользовательских сценариев поведения, необходимо использование браузерной среды или браузерной среды с программируемым API, что доступно в рамках Protractor, Nightwatch, Phantom.js, Сasper.

Описание некоторых фреймворков

JSDOM является реализацией JavaScript-стандартов WHATWG DOM и HTML. Другими словами, JSDom имитирует среду браузера, не запуская ничего, кроме простого JS. В этой моделируемой среде браузера тесты могут выполняться очень быстро. Недостатком JSDom является то, что не все может быть смоделировано вне реального браузера (например, вы не можете сделать снимок экрана), поэтому его использование ограничивает доступность ваших тестов.

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

Phantom.js — реализует «headless» браузер Webkit, который находится между реальным браузером и JSDom в скорости и стабильности. Достаточно популярен.

Karma — позволяет запускать тесты в браузерах, включая настоящие браузеры, Phantom, JSdom и даже устаревшие браузеры. Karma размещает тестовый сервер со специальной веб-страницей для запуска тестов в среде страницы. Эта страница может быть запущена во многих браузерах. Это также означает, что тесты можно запускать удаленно с помощью таких служб, как BrowserStack.

Chai — самая популярная assertion библиотека.

Unexpected — это также assertion библиотека с немного отличающимся синтаксисом от Chai.

Sinon.js — это набор очень мощных тестовых шпионов, заглушек и макетов (mocks) для модульного тестирования.

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

Jasmine — представляет собой платформу тестирования, обеспечивающую все, что вам требуется для ваших тестов: работающая среда, структура, отчетность, assertion и mocks инструменты.

Mocha — в настоящее время является наиболее часто используемой библиотекой. В отличие от Jasmine, она используется со сторонними библиотеками mocks и assertions (обычно Enzyme и Chai). Это означает, что Mocha немного сложнее настроить, но она более гибкая и открыта для расширений.

Jest — это платформа тестирования, рекомендованная Facebook. Он использует функционал Jasmine и добавляет функции поверх него, поэтому все упоминания о Jasmine относится и к нему.

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

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

Protractor — это библиотека, которая использует Selenium, но добавляет улучшенный синтаксис и специально встроенные хуки для Angular.

Nightwatch — имеет собственную реализацию selenium WebDriver. И обеспечивает собственную среду тестирования, тестовый сервер, assertion и другие инструменты.

Сasper — написан поверх Phantom и Slimer (так же, как Phantom, но в Gecko FireFox), чтобы при помощи специальных утилиты более просто создавать Phantom и Slimer скрипты. Каспер предоставляет нам более быстрый, но менее стабильный способ запуска функциональных тестов в браузерах с интерфейсом UI.

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

Литература:

Книги

The Art of Unit Testing by Roy Osherove

xUnit Test Patterns: Refactoring Test Code by Gerard Meszaros

Working Effectively with Legacy Code by Michael Feathers

Pragmatic Unit Testing in C# with NUnit, 2nd Edition by Andy Hunt and Dave Thomas

Статьи

https://www.sitepoint.com/javascript-testing-unit-functional-integration/

https://habrahabr.ru/post/169381/

https://joshldavis.com/2013/05/27/difference-between-tdd-and-bdd/

https://adamcod.es/2014/05/15/test-doubles-mock-vs-stub.html

https://www.quora.com/Software-Testing-What-is-the-difference-between-Mocks-and-Stubs

https://medium.com/powtoon-engineering/a-complete-guide-to-testing-javascript-in-2017-a217b4cd5a2a

https://blog.jscrambler.com/testing-apis-mocha-2/

https://scotch.io/tutorials/test-a-node-restful-api-with-mocha-and-chai

Подкасты

The History of JUnit and the Future of Testing with Kent Beck

Kent Beck, Developer Testing

--

--