Статьи

Тестирование JavaScript: модульные и функциональные против интеграционных тестов

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

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

Стоимость пренебрежения тестами

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

Разработка через тестирование требует немного больше времени, но ошибки, достигающие клиентов, во многом обходятся дороже:

  • Они прерывают взаимодействие с пользователем, что может стоить вам продаж, показателей использования, они могут даже навсегда отогнать клиентов.
  • Каждый отчет об ошибке должен быть проверен QA или разработчиками.
  • Исправления ошибок — это прерывания, которые вызывают дорогостоящее переключение контекста. Каждое прерывание может тратить до 20 минут на ошибку, не считая фактического исправления.
  • Диагностика ошибок происходит вне обычного контекста разработки функций, иногда разными разработчиками, которые не знакомы с кодом и его последствиями.
  • Стоимость возможности: команда разработчиков должна дождаться исправления ошибок, прежде чем они смогут продолжить работу над запланированным планом развития.

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

Различные типы тестов

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

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

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

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

Тесты ролей играют в непрерывной доставке

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

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

  • Модульные тесты гарантируют, что отдельные компоненты приложения работают как положено. Утверждения проверяют компонент API.
  • Интеграционные тесты обеспечивают совместную работу компонентов, как и ожидалось. Утверждения могут проверять API компонента, пользовательский интерфейс или побочные эффекты (такие как ввод-вывод базы данных, ведение журнала и т. Д.)
  • Функциональные тесты гарантируют, что приложение работает должным образом с точки зрения пользователя. Утверждения в первую очередь проверяют пользовательский интерфейс.

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

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

Какие типы тестов следует использовать? Все они.

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

  • Тесты пользовательского опыта (опыт конечного пользователя)
  • Тесты API разработчика (опыт разработчика)
  • Тесты инфраструктуры (нагрузочные тесты, тесты сетевой интеграции и т. Д.)

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

Тесты API разработчика исследуют систему с точки зрения разработчика. Когда я говорю API, я не имею в виду HTTP API. Я имею в виду API-интерфейс поверхности объекта: интерфейс, используемый разработчиками для взаимодействия с модулем, функцией, классом и т. Д.

Модульные тесты: отзывы разработчиков в реальном времени

Модульные тесты гарантируют, что отдельные компоненты работают изолированно друг от друга. Единицы — это, как правило, модули, функции и т. Д.

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

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

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

Запуск тестов на изменение файла

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

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

Модульные тесты должны быть:

  • Смертельно просто.
  • Молниеносно.
  • Хороший отчет об ошибке.

Что я имею в виду под «хорошим сообщением об ошибке»?

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

  1. Какой компонент тестируется?
  2. Какое поведение ожидается?
  3. Каков был фактический результат?
  4. Каков ожидаемый результат?
  5. Как воспроизводится поведение?

Первые четыре вопроса должны быть видны в отчете об ошибках. Последний вопрос должен быть понятен из реализации теста. Некоторые типы утверждений не способны ответить на все эти вопросы в отчете об deepEqual , но большинство equal , same или deepEqual утверждений должны. Фактически, если бы это были единственные утверждения в какой-либо библиотеке утверждений, большинство тестовых наборов, вероятно, было бы лучше. Упростить.

Вот несколько простых примеров модульного тестирования из реальных проектов с использованием Tape :

 // Ensure that the initial state of the "hello" reducer gets set correctly import test from 'tape'; import hello from 'store/reducers/hello'; test('...initial', assert => { const message = `should set { mode: 'display', subject: 'world' }`; const expected = { mode: 'display', subject: 'World' }; const actual = hello(); assert.deepEqual(actual, expected, message); assert.end(); }); 
 // Asynchronous test to ensure that a password hash is created as expected. import test from 'tape', import credential from '../credential'; test('hash', function (t) { // Create a password record const pw = credential(); // Asynchronously create the password hash pw.hash('foo', function (err, hash) { t.error(err, 'should not throw an error'); t.ok(JSON.parse(hash).hash, 'should be a json string representing the hash.'); t.end(); }); }); 

Интеграционные тесты

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

В этом случае у нас есть два тестируемых блока:

  1. Обработчик маршрута
  2. Регистратор

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

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

Давайте посмотрим на это более подробно. Обработчик маршрута — это фабричная функция, которая использует внедрение зависимостей для внедрения регистратора в обработчик маршрута. Давайте посмотрим на подпись (см. Документацию по rtype для помощи в чтении подписей):

 createRoute({ logger: LoggerInstance }) => RouteHandler 

Давайте посмотрим, как мы можем проверить это:

 import test from 'tape'; import createLog from 'shared/logger'; import routeRoute from 'routes/my-route'; test('logger/route integration', assert => { const msg = 'Logger logs router calls to memory'; const logMsg = 'hello'; const url = `http://127.0.0.1/msg/${ logMsg }`; const logger = createLog({ output: 'memory' }); const routeHandler = createRoute({ logger }); routeHandler({ url }); const actual = logger.memoryLog[0]; const expected = logMsg; assert.equal(actual, expected, msg); assert.end(); }); 

Мы пройдемся по важным деталям более подробно. Во-первых, мы создаем регистратор и говорим ему войти в память:

 const logger = createLog({ output: 'memory' }); 

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

 const routeHandler = createRoute({ logger }); 

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

 routeHandler({ url }); 

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

  const actual = logger.memoryLog[0]; 

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

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

Функциональные тесты

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

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

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

Функциональные тесты должны иметь возможность запускаться в облаке на таких сервисах, как Sauce Labs , которые обычно используют API WebDriver в таких проектах, как Selenium.

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

Мой любимый это Nightwatch.js . Вот как выглядит простой набор функциональных тестов Nightwatch в этом примере из документации Nightwatch:

 module.exports = { 'Demo test Google' : function (browser) { browser .url('http://www.google.com') .waitForElementVisible('body', 1000) .setValue('input[type=text]', 'nightwatch') .waitForElementVisible('button[name=btnG]', 1000) .click('button[name=btnG]') .pause(1000) .assert.containsText('#main', 'Night Watch') .end(); } }; 

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

Тесты дыма

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

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

Дымовые тесты — не единственное применение для функциональных тестов, но, на мой взгляд, они наиболее ценные.

Что такое непрерывная доставка?

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

  1. Сбор требований
  2. дизайн
  3. Реализация
  4. верификация
  5. развертывание
  6. техническое обслуживание

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

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

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

Решение для непрерывной доставки

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

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

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

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

Вывод

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

Как вы используете автоматизированные тесты в своем коде, и как это влияет на вашу уверенность и производительность? Дай мне знать в комментариях.