Корень медленных тестов в рельсах
Выполнение тестов в Rails обычно происходит медленно, и медлительность часто возникает из самой первой строки большинства файлов спецификаций — require 'spec_helper.rb'
.
Возьмем типичный spec_helper.rb
например (из gitlab ):
Простое изучение этого файла показывает его обязанности по загрузке:
— Вся среда Rails на L14
— Тестовые фреймворки и их конфигурация, такие как rspec
и rspec
— Помощники по тестированию и их конфигурации, такие как email_spec
, factorygirl
и webmock
— Тест репортеров и акселераторов — simplecov
, simplecov
Это много кода для загрузки, и он загружается независимо от контекста теста. Приемочные тесты загружают его, интеграционные тесты загружают его, модульные тесты также загружают его. Это один для всех spec_helper.rb
способствует ненужному времени загрузки, когда нам нужно выполнить какой-либо отдельный тест.
Нужно ли нам так много?
В процессе разработки через тестирование наиболее частыми тестами, которые нам нужно запустить во время разработки, являются юнит-тесты. Нам нужно только запустить интеграционные тесты и приемочные тесты к концу цикла разработки.
Более того, при хороших шаблонах проектирования большинство объектов будет построено на PORO (Plain Old Ruby Object), таких как Объект Значения, Объект Политики или Объект Формы . Эти объекты основаны на API-интерфейсах платформы и могут тестироваться отдельно от среды.
Чтобы протестировать эти PORO, нам не нужно загружать всю платформу Rails. Нам просто нужно выбрать необходимые компоненты для запуска тестов. Это излечивает основную причину медленных тестов.
Вот рабочая демонстрация, иллюстрирующая эту идею.
git clone https://github.com/yangchenyun/lite_spec_helper_demo
Он содержит источник и спецификации валидатора модели Rails, который наследуется от ActiveModel::Validators
.
Папка содержит обычную структуру Rails, но пропускает весь код, кроме двух соответствующих файлов: проверяемого валидатора и спецификаций валидатора.
rspec spec/models
, вы обнаружите, что он выполняет три теста. Это минимальные требования для запуска теста.
$ rspec spec/ ... Finished in 0.02124 seconds 3 examples, 0 failures
Научитесь запускать тесты без Rails
Быстрый взгляд на этот файл спецификации покажет несколько строк, которые обычно не появляются в таких файлах:
# ... require 'active_model' require 'active_support/core_ext/object' require_relative '../../app/models/order_delivery_date_validator' # ...
Два require
s в приведенном выше фрагменте — это зависимости, необходимые для запуска валидатора в спецификации. Эта дополнительная работа необходима, потому что мы не загружаем Rails и всю его магию под капотом.
Во-первых, по умолчанию не загружаются компоненты Rails — ни ActiveSupport
, ни ActionController
, ни ActionController
, ни код нашего проекта. Во-вторых, ActiveSupport::Autoload
здесь не используется, поэтому мы не можем полагаться на Rails, чтобы угадать имена объектов и загрузить правильный исходный файл. Ответственность за загрузку необходимых файлов теперь лежит на нас.
require 'active_model'
в этот файл и делает ActiveModel::Validator
доступным.
require 'active_support/core_ext'
расширяет Object
с blank?
и Fixnum
с days
.
Эта ручная работа также приносит еще одно скрытое преимущество. Внешние API открыты и предоставляют информацию о контексте, в котором живет наш объект.
Наконец, require_relative '../../app/models/order_delivery_date_validator'
загружает исходный код для тестирования по относительному пути.
Построить минимальный spec_helper_lite.rb
Теперь мы начинаем вычитать то, что является общим для нескольких похожих спецификаций.
Строка require_relative
является хорошей точкой для начала. ../..
слишком многословен и затрудняет поддержку, если мы изменим структуру папок. Что если бы нам потребовалось вернуть исходный код более кратким способом, таким как require 'order_delivery_date_validator'
?
Это может быть достигнуто путем изменения $:
( $LOAD_PATH
) в Ruby. Когда нам require 'something'
, Ruby просматривает все пути в $:
и ищет something.rb
для загрузки в соответствии с ruby-doc :
Если имя файла не преобразуется в абсолютный путь, его будут искать в каталогах, перечисленных в $ LOAD_PATH ($ :). Если имя файла имеет расширение «.rb», оно загружается как исходный файл…
Чтобы включить этот краткий синтаксис, мы просто добавляем каталог app/models
в $:
Мы также можем добавить любые каталоги, которые содержат другой источник, используя эту конфигурацию.
# spec/spec_helper_lite.rb # ... $:.unshift File.expand_path '../../app/models', __FILE__
Теперь мы можем использовать этот легкий помощник в наших спецификациях. Вместо использования require_relative
с длинным путем с большим количеством ..
, мы могли бы просто require 'order_delivery_date_validator'
. Кроме того, в будущем мы всегда можем добавить в этот «облегченный» помощник часто необходимые файлы для большинства спецификаций.
Вот обновленное репо с этим вновь созданным spec_helper_lite.rb
.
Меньше — больше
По сравнению с многофункциональным типичным spec_helper.rb
наш spec_helper_lite.rb
минимален. Вместо того, чтобы загружать все служебные данные фреймворка, он загружает только тестовый фреймворк rspec
и rspec
$:
для облегчения загрузки исходного кода нашего проекта.
Этот spec_helper.rb
поддерживает модульное тестирование и намного быстрее оригинального. Кроме того, он выявляет зависимости в спецификациях и обнаруживает слишком сложные объекты.
spec_helper.rb
все еще может быть загружен для интеграционных и приемочных тестов, но spec_helper_lite.rb
сделает вас счастливее в ваших часто выполняемых модульных тестах.
Вывод
Из минимального требования для запуска модульных тестов против объекта в Rails мы создали облегченную версию spec_helper.rb
. Он предоставляет минимальные утилиты для загрузки файлов и выявляет объектные зависимости, ускоряя юнит-тесты.