Подтвердить что ты не робот

Тестирование: как сосредоточиться на поведении вместо реализации, не теряя скорость?

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

Дело в том, что эти мнения были высказаны 5 лет назад (2007), и мне интересно, что изменилось с тех пор и в каком направлении я должен идти.

Хранители Брэндона:

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

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

Это само по себе не так уж плохо, но проблема в том, что он тоже много в контроллер, чтобы диктовать, как используется модель. Почему это вопрос, если мой контроллер вызывает Thing.new? Что делать, если мой контроллер решает взять Thing.create! и спасательный маршрут? Что делать, если моя модель имеет специальный метод инициализации, например Thing.build_with_foo? Моя спецификация для поведение не должно прерываться, если я изменю реализацию.

Эта проблема становится еще хуже, когда у вас есть вложенные ресурсы и создание нескольких моделей на контроллер. Некоторые мои методы настройки завершаются до 15 или более линий длиной и ОЧЕНЬ хрупкой.

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

Итак, что делать BDD-wannabe? Сделав шаг назад, поведение, которое я действительно хочу проверить не то, что мой контроллер вызывает Thing.new, но что при заданных параметрах X он создает новую вещь и перенаправляет на нее.

Дэвид Челимский:

Все о компромиссах.

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

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

На практике я запускаю тесты сотен, если не тысячи, раз день (я использую автотест и делаю очень подробные шаги), и я изменяю, Я использую "новый" или "создаю" почти никогда. Также из-за гранулированных стадий, новые модели, которые появляются, сначала являются волатильными. Действительный_что_attrs подход немного уменьшает боль от этого, но это все еще означает, что каждое новое обязательное поле означает, что я должен изменить valid_thing_attrs.

Но если ваш подход работает на вас на практике, тогда это хорошо! В факт, я настоятельно рекомендую опубликовать плагин с генераторами которые приводят примеры так, как вам нравятся. Я уверен, что много людей выиграют от этого.

Райан Бейтс:

Из любопытства, как часто вы используете mocks в своих тестах/спецификациях? Возможно, я делаю что-то неправильно, но я нахожу это серьезным ограничение. Начиная с перехода на rSpec более месяца назад, я делал что они рекомендуют в документах, где уровни контроллера и просмотра вообще не попадайте в базу данных, и модели полностью издеваются вне. Это дает вам хорошее ускорение скорости и облегчает некоторые вещи, но я нахожу, что минусы этого намного превосходят профессионалов. поскольку используя издевательства, мои спецификации превратились в кошмар для обслуживания. Спекуляции предназначены для проверки поведения, а не для реализации. Мне все равно если был вызван метод, я просто хочу убедиться, что полученный результат верно. Потому что насмешливая специфика реализации, он делает простые рефакторинги (которые не меняют поведение) невозможно без постоянного возвращения назад и "исправить" спецификации. Я очень убежден в том, что спецификация/тесты должны обложка. Тест прерывается только тогда, когда приложение ломается. Это один причина, по которой я вряд ли тестирую слой обзора, потому что считаю его слишком жестким. Это часто приводит к разрыву тестов без нарушения приложения. изменяя мелочи в представлении. Я нахожу ту же проблему с издевается. Помимо всего прочего, я сегодня понял, что насмешливое/прерывистое метод класса (иногда) держится между спецификациями. Спецификации должны быть автономным и не под влиянием других спецификаций. Это нарушает правило и приводит к сложным ошибкам. Что я узнал из всего этого? Быть осторожно, когда вы используете насмешку. Stubbing не так плохо, но все же некоторые из тех же проблем.

Я взял последние несколько часов и удалил почти все издевательства из моих спецификаций. Я также объединил контроллер и просмотрел спецификации в один, используя "integrate_views" в спецификации контроллера. Я также загружаю все светильники для каждой спецификации контроллера, поэтому есть некоторые тестовые данные для заполнения Виды. Конечный результат? Мои спецификации короче, проще, больше непротиворечивые, менее жесткие, и они проверяют весь стек вместе (модель, вид, контроллер), поэтому никакие ошибки не могут проскочить через трещины. я не говоря о том, что это "правильный" способ для всех. Если ваш проект требует очень строгой спецификации, тогда это может быть не для вас, но в моем это мир лучше, чем то, что у меня было до использования mocks. я по-прежнему думаю, что stubbing - хорошее решение в нескольких местах, поэтому я все еще делаю что.

4b9b3361

Ответ 1

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

Но эти компромиссы являются симптомами более глубокой проблемы, о которой Давид упомянул в 2007 году: ActiveRecord. Дизайн ActiveRecord поощряет вас создавать объекты-боги, которые слишком много делают, слишком много знают о остальной части системы и имеют слишком большую площадь поверхности. Это приводит к испытаниям, которые слишком много для проверки, слишком много знают об остальной системе и слишком медленны или хрупки.

Итак, какое решение? Отделите как можно больше от вашего приложения из рамок. Напишите много небольших классов, которые моделируют ваш домен и не наследуют ни от чего. Каждый объект должен иметь ограниченную площадь поверхности (не более нескольких методов) и явные зависимости, переданные через конструктор.

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

К сожалению, я не могу указать вам пример проекта, который делает это хорошо (пока). Я немного об этом расскажу в своем выступлении на Почему наш код пахнет, посмотрите презентацию Кори Хейнса на Fast Rails Tests, и я настоятельно рекомендую читать Растущее объектно-ориентированное программное обеспечение, управляемое тестами.

Ответ 2

Спасибо за компиляцию цитат из 2007. Приятно оглянуться назад.

Мой текущий подход к тестированию описан в этом эпизоде ​​RailsCasts, который мне очень понравился. Таким образом, у меня есть два уровня тестов.

  • Высокий уровень: Я использую спецификации запросов в RSpec, Capybara и VCR. Тесты могут быть отмечены для выполнения JavaScript по мере необходимости. Издевательства здесь исключаются, потому что цель состоит в том, чтобы протестировать весь стек. Каждое действие контроллера проверяется хотя бы один раз, может быть, несколько раз.

  • Низкий уровень:. Здесь тестируется вся сложная логика - в первую очередь модели и помощники. Я также избегаю насмешек. При необходимости тесты попадают в базу данных или окружающие объекты.

Обратите внимание, что нет спецификаций контроллера или просмотра. Я считаю, что они адекватно охвачены спецификациями запросов.

Так как есть немного насмешливо, как быстро сохранить тесты? Вот несколько советов.

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

  • При создании записей (например, с помощью Factory Девушка) используйте build сначала и только при необходимости переключитесь на create.

  • Используйте Guard с Spork, чтобы пропустить время запуска Rails. Соответствующие тесты часто выполняются в течение нескольких секунд после сохранения файла. Используйте тег :focus в RSpec, чтобы ограничить, какие тесты выполняются при работе в определенной области. Если это большой набор тестов, установите all_after_pass: false, all_on_start: false в Guardfile, чтобы запускать их только при необходимости.

  • Я использую несколько утверждений за тест. Выполнение одного и того же кода установки для каждого утверждения значительно увеличит время тестирования. RSpec распечатает строку, которая не удалась, поэтому ее легко найти.

Я нахожу насмешливый, добавляет хрупкость к испытаниям, поэтому я избегаю этого. Правда, это может быть здорово как помощь для дизайна OO, но в структуре Rails-приложения это не так эффективно. Вместо этого я сильно полагаюсь на рефакторинг, и пусть сам код скажет мне, как дизайн должен идти.

Этот подход лучше всего подходит для приложений Rails малого и среднего размера без обширной сложной логики домена.

Ответ 3

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

  • Единичные тесты: изолированные тесты, как правило, но не всегда, против простых рубиновых объектов. Мои модульные тесты не связаны с вызовами DB, сторонних API-интерфейсов или любыми другими внешними материалами.
  • Интеграционные тесты: они все еще сосредоточены на тестировании одного класса; различия заключаются в том, что они интегрируют этот класс с внешним материалом, которого я избегаю в своих модульных тестах. Мое модели часто имеют как модульные тесты, так и интеграционные тесты, когда тесты модулей фокусируются на чистой логике, которые могут быть протестированы без использования БД, а тесты интеграции будут включать БД. Кроме того, я склонен тестировать сторонние API-обертки с интеграционными тестами, используя VCR, чтобы тесты были быстрыми и детерминированными, но позволяя моим CI-сборкам делать HTTP-запросы реальными (чтобы уловить любые изменения API).
  • Приемочные тесты: сквозные тесты для всей функции. Речь идет не только о тестировании пользовательского интерфейса через capybara; Я делаю то же самое в своих драгоценных камнях, у которых вообще нет HTML-интерфейса. В этих случаях это упражнение независимо от того, что самоцветов делает от конца до конца. Я также склонен использовать VCR в этих тестах (если они делают внешние HTTP-запросы), и, как и в моих тестах интеграции, моя сборка CI настроена так, чтобы HTTP-запросы были реальными.

Насколько насмехается, у меня нет подхода "одного размера подходит всем". Я определенно перехитрил в прошлом, но все же считаю, что это очень полезная техника, особенно при использовании чего-то вроде rspec-fire. В общем, я притворяюсь, что коллабораторы свободно играют в роли (особенно, если я их владею, и они являются объектами службы) и стараются избегать этого в большинстве других случаев.

Вероятно, самое большое изменение в моем тестировании за последний год или около того было вдохновлено DAS: в то время как у меня был spec_helper.rb который загружает всю среду, теперь я явно загружаю только тест класса (и любые зависимости). Помимо улучшенной скорости тестирования (что делает огромную разницу!), Это помогает мне идентифицировать, когда мой класс-под-тест тянет слишком много зависимостей.