Руководство по тестированию приложений на Rails

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

После его прочтения, вы узнаете:

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

1. Зачем писать тесты для вашего приложения на Rails?

Rails предлагает писать тесты очень просто. Когда вы создаете свои модели и контроллеры, он начинает создавать скелет тестового кода.

Запуск тестов Rails позволяет убедиться, что ваш код придерживается нужной функциональности даже после большой переделки кода.

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

2. Введение в тестирование

Поддержка тестирования встроена в Rails с самого начала. И это не было так: "О! Давайте внесем поддержку запуска тестов, это ново и круто!"

2.1. Настройка Rails для тестирования с нуля

Rails создает директорию test как только вы создаете проект Rails, используя rails new _application_name_. Если посмотрите список содержимого этой папки, то увидите:

$ ls -F test

controllers/           helpers/               mailers/               system/                test_helper.rb
fixtures/              integration/           models/                application_system_test_case.rb

Директории helpers, mailers и models предназначены содержать тесты для хелперов вьюх, рассыльщиков и моделей соответственно. Директория controllers предназначена содержать тесты для ваших контроллеров, маршрутов и вьюх. Директория integration предназначена содержать тесты для взаимодействия между контроллерами.

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

Фикстуры это способ организации тестовых данных; они находятся в директории fixtures.

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

Файл test_helper.rb содержит конфигурацию по умолчанию для ваших тестов.

application_system_test_case.rb содержит настройки по умолчанию для ваших системных тестов.

2.2. Тестовая среда разработки

По умолчанию каждое приложение на Rails имеет три среды разработки: development, test и production. База данных для каждой из них настраивается в config/database.yml.

Схожим образом можно изменить конфигурацию среды. В этом случае можно изменить тестовую среду, изменяя опции в config/environments/test.rb.

Ваши тесты запускаются с RAILS_ENV=test.

2.3. Rails встретился с Minitest

Если помните, мы использовали команду rails generate model в руководстве Rails для начинающих. Мы создали нашу первую модель, где, среди прочего, создались незаконченные тесты в папке test:

$ bin/rails generate model article title:string body:text
...
create  app/models/article.rb
create  test/models/article_test.rb
create  test/fixtures/articles.yml
...

Незаконченный тест по умолчанию в test/models/article_test.rb выглядит так:

require 'test_helper'

class ArticleTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end

Построчное изучение этого файла поможет ориентироваться в коде тестирования и терминологии Rails.

require 'test_helper'

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

class ArticleTest < ActiveSupport::TestCase

Класс ArticleTest определяет тестовый случай, поскольку он унаследован от ActiveSupport::TestCase. Поэтому ArticleTest имеет все методы, доступные в ActiveSupport::TestCase. Позже в этом руководстве мы увидим некоторые из методов, которые он нам дает.

Любой метод, определенный в классе, унаследованном от Minitest::Test (который является суперклассом для ActiveSupport::TestCase), начинающийся с test_ (чувствительно к регистру), просто вызывает тест. Таким образом, методы, определенные как test_password и test_valid_password, это правильные имена тестов, и запустятся автоматически при запуске тестового случая.

Rails также добавляет метод test, который принимает имя теста и блок. Он генерирует обычный тест MiniTest::Unit с именем метода, начинающегося с test_, поэтому можно не беспокоиться об именовании методов, а просто писать так:

test "the truth" do
  assert true
end

Это является приблизительно тем же, как если бы написали:

def test_the_truth
  assert true
end

Хотя вы все еще можете использовать обычные определения методов, использование макроса test позволяет получить более читаемое имя теста.

Имя метода генерируется, заменяя пробелы на подчеркивания. Хотя результат не должен быть валидным идентификатором Ruby, имя может содержать знаки пунктуации и т.д. Это связано с тем, что в Ruby технически любая строка может быть именем метода. Это может потребовать, чтобы вызовы define_method и send функционировали правильно, но формально есть только небольшое ограничение на имя.

Далее посмотрим на наше первое утверждение:

assert true

Утверждение (assertion) - это строчка кода, которая вычисляет объект (или выражение) для ожидаемых результатов. Например, утверждение может проверить:

  • является ли это значение равным тому значению?
  • является ли этот объект nil?
  • вызывает ли эта строчка кода исключение?
  • является ли пароль пользователя больше, чем 5 символов?

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

2.3.1. Ваш первый падающий тест

Чтобы увидеть, как сообщается провал теста, вы можете добавить проваливающийся тест в тестовый случай article_test.rb.

test "should not save article without title" do
  article = Article.new
  assert_not article.save
end

Давайте запустим только что добавленный тест (где 6 - это номер строчки, где определен тест).

$ bin/rails test test/models/article_test.rb:6
Run options: --seed 44656

# Running:

F

Failure:
ArticleTest#test_should_not_save_article_without_title [/path/to/blog/test/models/article_test.rb:6]:
Expected true to be nil or false


bin/rails test test/models/article_test.rb:6


Finished in 0.023918s, 41.8090 runs/s, 41.8090 assertions/s.

1 runs, 1 assertions, 1 failures, 0 errors, 0 skips


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

test "should not save article without title" do
  article = Article.new
  assert_not article.save, "Saved the article without a title"
end

Запуск этого теста покажет более дружелюбное сообщение для утверждения:

Failure:
ArticleTest#test_should_not_save_article_without_title [/path/to/blog/test/models/article_test.rb:6]:
Saved the article without a title

Теперь, чтобы этот тест прошел, можно добавить валидацию на уровне модели для поля title.

class Article < ApplicationRecord
  validates :title, presence: true
end

Теперь тест пройдет. Давайте убедимся в этом, запустив его снова:

$ bin/rails test test/models/article_test.rb:6
Run options: --seed 31252

# Running:

.
Finished in 0.027476s, 36.3952 runs/s, 36.3952 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

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

2.3.2. Как выглядит ошибка

Чтобы увидеть, как сообщается об ошибке, вот тест, содержащий ошибку:

test "should report error" do
  # переменная some_undefined_variable не определена в другом месте тестового случая
  some_undefined_variable
  assert true
end

Теперь вы увидите чуть больше результата в консоли от запуска тестов:

$ bin/rails test test/models/article_test.rb
Run options: --seed 1808

# Running:

.E

Error:
ArticleTest#test_should_report_error:
NameError: undefined local variable or method 'some_undefined_variable' for #<ArticleTest:0x007fee3aa71798>
    test/models/article_test.rb:11:in 'block in <class:ArticleTest>'


bin/rails test test/models/article_test.rb:9



Finished in 0.040609s, 49.2500 runs/s, 24.6250 assertions/s.

2 runs, 1 assertions, 0 failures, 1 errors, 0 skips

Отметьте 'E' в результате. Она отмечает тест с ошибкой.

Запуск каждого тестового метода останавливается, как только возникают какие-либо ошибки или провал утверждения, и тестовый набор продолжается со следующего метода. Все тестовые методы запускаются в случайном порядке. Для настройки порядка тестирования может быть использована опция config.active_support.test_order.

Когда тест проваливается, вам показывается соответствующий бэктрейс. По умолчанию Rails фильтрует этот бэктрейс и печатает только строчки, относящиеся к вашему приложению. Это устраняет шум от фреймворка и помогает сфокусироваться на вашем коде. Однако, бывают ситуации, когда вам захочется увидеть полный бэктрейс. Установите аргумент -b (или --backtrace) для включения этого поведения:

$ bin/rails test -b test/models/article_test.rb

Если хотите, чтобы этот тест прошел, можно его изменить, используя assert_raises следующим образом:

test "should report error" do
  # переменная some_undefined_variable не определена в тесте
  assert_raises(NameError) do
    some_undefined_variable
  end
end

Теперь этот тест должен пройти.

2.4. Доступные утверждения

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

Ниже представлена выдержка утверждений, которые вы можете использовать с Minitest, библиотекой тестирования, используемой Rails по умолчанию. Параметр [msg] - это опциональное строковое сообщение, которое вы можете указать для того, чтобы сделать сообщение о провале более ясным.

Утверждение Назначение
assert( test, [msg] ) Утверждает, что test истинно.
assert_not( test, [msg] ) Утверждает, что test ложно.
assert_equal( expected, actual, [msg] ) Утверждает, что expected == actual истинно.
assert_not_equal( expected, actual, [msg] ) Утверждает, что expected != actual истинно.
assert_same( expected, actual, [msg] ) Утверждает, что expected.equal?(actual) истинно.
assert_not_same( expected, actual, [msg] ) Утверждает, что expected.equal?(actual) ложно.
assert_nil( obj, [msg] ) Утверждает, что obj.nil? истинно.
assert_not_nil( obj, [msg] ) Утверждает, что obj.nil? ложно.
assert_empty( obj, [msg] ) Утверждает, что obj является empty?.
assert_not_empty( obj, [msg] ) Утверждает, что obj не является empty?.
assert_match( regexp, string, [msg] ) Утверждает, что строка соответствует регулярному выражению.
assert_no_match( regexp, string, [msg] ) Утверждает, что строка не соответствует регулярному выражению.
assert_includes( collection, obj, [msg] ) Утверждает, что obj находится в collection.
assert_not_includes( collection, obj, [msg] ) Утверждает, что obj не находится в collection.
assert_in_delta( expected, actual, [delta], [msg] ) Утверждает, что между числами expected и actual разницу delta.
assert_not_in_delta( expected, actual, [delta], [msg] ) Утверждает, что между числами expected и actual разница, отличная от delta.
assert_in_epsilon ( expected, actual, [epsilon], [msg] ) Утверждает, что между числами expected и actual относительная погрешность меньше, чем epsilon.
assert_not_in_epsilon ( expected, actual, [epsilon], [msg] ) Утверждает, что между числами expected и actual относительная погрешность не меньше, чем epsilon.
assert_throws( symbol, [msg] ) { block } Утверждает, что переданный блок бросает symbol.
assert_raises( exception1, exception2, ... ) { block } Утверждает, что переданный блок генерирует одно из переданных исключений.
assert_instance_of( class, obj, [msg] ) Утверждает, что obj является экземпляром class.
assert_not_instance_of( class, obj, [msg] ) Утверждает, что obj не является экземпляром class.
assert_kind_of( class, obj, [msg] ) Утверждает, что obj является экземпляром class или класса, наследуемого от него.
assert_not_kind_of( class, obj, [msg] ) Утверждает, что obj не является экземпляром class или класса, наследуемого от него.
assert_respond_to( obj, symbol, [msg] ) Утверждает, что obj отвечает на symbol.
assert_not_respond_to( obj, symbol, [msg] ) Утверждает, что obj не отвечает на symbol.
assert_operator( obj1, operator, [obj2], [msg] ) Утверждает, что obj1.operator(obj2) истинно.
assert_not_operator( obj1, operator, [obj2], [msg] ) Утверждает, что obj1.operator(obj2) ложно.
assert_predicate ( obj, predicate, [msg] ) Утверждает, что obj.predicate истинно, т.е. assert_predicate str, :empty?
assert_not_predicate ( obj, predicate, [msg] ) Утверждает, что obj.predicate ложно, т.е. assert_not_predicate str, :empty?
flunk( [msg] ) Утверждает провал. Это полезно для явного указания теста, который еще не закончен.

Представленный выше список утверждений поддерживается minitest. Более полный и более актуальный список всех доступных утверждений смотрите в документации Minitest API, в частности Minitest::Assertions

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

Создание собственных утверждений это особый разговор, которого мы касаться не будем.

2.5. Специфичные утверждения Rails

Rails добавляет некоторые свои утверждения в фреймворк minitest:

Утверждение Назначение
assert_difference(expressions, difference = 1, message = nil) {...} Тестирует числовую разницу между возвращаемым значением expression и результатом вычисления в данном блоке.
assert_no_difference(expressions, message = nil, &block) Утверждает, что числовой результат вычисления expression не изменяется до и после применения переданного в блоке.
assert_changes(expressions, message = nil, from:, to:, &block) Тестирует, что результат вычисления expression изменится после применения переданного в блоке.
assert_no_changes(expressions, message = nil, &block) Тестирует, что результат вычисления expression не изменится после применения переданного в блоке.
assert_nothing_raised { block } Обеспечивает, что данный блок не вызывает какие-либо исключения.
assert_recognizes(expected_options, path, extras={}, message=nil) Обеспечивает, что роутинг данного path был правильно обработан, и что проанализированные опции (заданные в хэше expected_options) соответствуют path. По существу он утверждает, что Rails распознает маршрут, заданный в expected_options.
assert_generates(expected_path, options, defaults={}, extras = {}, message=nil) Утверждает, что предоставленные options могут быть использованы для генерации предоставленного пути. Это противоположность assert_recognizes. Параметр extras используется, чтобы сообщить запросу имена и значения дополнительных параметров запроса, которые могут быть в строке запроса. Параметр message позволяет определить свое сообщение об ошибке при провале утверждения.
assert_response(type, message = nil) Утверждает, что отклик идет с определенным кодом статуса. Можете определить :success для обозначения 200-299, :redirect для обозначения 300-399, :missing для обозначения 404, или :error для соответствия интервалу 500-599. Можно передать явный номер статуса или его символьный эквивалент. Более подробно смотрите в полном списке кодов статуса и как работает их привязка.
assert_redirected_to(options = {}, message=nil) Утверждает, что опции перенаправления передаются в соответствии с вызовами перенаправления в последнем экшне. Это соответствие может быть частичным, так assert_redirected_to(controller: "weblog") будет также соответствовать перенаправлению redirect_to(controller: "weblog", action: "show") и тому подобное. Также можно передать именованные маршруты, как в assert_redirected_to root_path, и объекты Active Record, как в assert_redirected_to @article.

Вы увидите использование некоторых из этих утверждений в следующей части.

2.6. Краткая заметка о тестовых случаях

Все основные утверждения, такие как assert_equal, определенные в Minitest::Assertions, также доступны в классах, используемых в наших тестовых случаях. Фактически, Rails представляет вам следующие классы для наследования:

Каждый из этих классов включает Minitest::Assertions, позволяя использовать все основные утверждения в наших тестах.

За подробностями о Minitest обратитесь к его документации

2.7. Запуск тестов Rails

Можно запустить все тесты за раз с помощью команды bin/rails test.

Или можно запустить отдельный тест, передав команде bin/rails test имя файла, содержащего тестовые случаи.

$ bin/rails test test/models/article_test.rb
Run options: --seed 1559

# Running:

..

Finished in 0.027034s, 73.9810 runs/s, 110.9715 assertions/s.

2 runs, 3 assertions, 0 failures, 0 errors, 0 skips

Это запустит все тестовые методы из тестового случая.

Также можете запустить определенный тестовый метод из тестового случая, предоставив флажок -n или --name и имя метода теста.

$ bin/rails test test/models/article_test.rb -n test_the_truth
Run options: -n test_the_truth --seed 43583

# Running:

.

Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s.

1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

Также можно запустить тест в определенной строчке, предоставив номер строчки.

$ bin/rails test test/models/article_test.rb:6 # запускает определенный тест и строчку

Также можно запустить целую директорию тестов, предоставив путь к этой директории.

$ bin/rails test test/controllers # запускает все тесты из определенной директории

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

$ bin/rails test -h
minitest options:
    -h, --help                       Display this help.
    -s, --seed SEED                  Sets random seed. Also via env. Eg: SEED=n rake
    -v, --verbose                    Verbose. Show progress processing files.
    -n, --name PATTERN               Filter run on /regexp/ or string.
        --exclude PATTERN            Exclude /regexp/ or string from run.

Known extensions: rails, pride

Usage: bin/rails test [options] [files or directories]
You can run a single test by appending a line number to a filename:

    bin/rails test test/models/user_test.rb:27

You can run multiple files and directories at the same time:

    bin/rails test test/controllers test/integration/login_test.rb

By default test failures and errors are reported inline during a run.

Rails options:
    -w, --warnings                   Run with Ruby warnings enabled
    -e, --environment                Run tests in the ENV environment
    -b, --backtrace                  Show the complete backtrace
    -d, --defer-output               Output test failures and errors after the test run
    -f, --fail-fast                  Abort test run on first failure or error
    -c, --[no-]color                 Enable color in the output

3. Тестовая база данных

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

По умолчанию каждое приложение на Rails имеет три среды разработки: development, test и production. База данных для каждой из них настраивается в config/database.yml.

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

3.1. Поддержка схемы тестовой базы данных

Чтобы запустить тесты, ваша тестовая база данных должна иметь текущую структуры. Тестовый хелпер проверяет, не имеет ли ваша тестовая база данных отложенных миграций. Он пытается загрузить ваши db/schema.rb или db/structure.sql в тестовую базу данных. Если есть отложенные миграции - будет вызвана ошибка. Обычно это указывает на то, что ваша схема не полностью мигрирована. Запуск миграций для базы данных development (bin/rails db:migrate) приведет схему в актуальное состояние.

Если были изменения в существующих миграциях, нужно перестроить тестовую базу данных. Это делается с помощью выполнения bin/rails db:test:prepare.

3.2. Полная информация по фикстурам

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

3.2.1. Что такое фикстуры?

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

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

Фикстуры расположены в директории test/fixtures. Когда запускаете rails generate model для создания новой модели, Rails автоматически создаст незаконченные фикстуры в этой директории.

3.2.2. YAML

Фикстуры в формате YAML являются дружелюбным способом описать ваш образец данных. Этот тип фикстур имеет расширение файла .yml (как в users.yml).

Вот образец файла фикстуры YAML:

# lo & behold! I am a YAML comment!
david:
  name: David Heinemeier Hansson
  birthday: 1979-10-15
  profession: Systems development

steve:
  name: Steve Ross Kellock
  birthday: 1974-09-27
  profession: guy with keyboard

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

Если работаете со связями, можно определить ссылку между двумя различными фикстурами. Вот пример для связи belongs_to/has_many:

# In fixtures/categories.yml
about:
  name: About

# In fixtures/articles.yml
first:
  title: Welcome to Rails!
  body: Hello world!
  category: about

Отметьте, что у ключа category в статье first из fixtures/articles.yml значение about. Это говорит Rails загрузить категорию about из fixtures/categories.yml.

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

3.2.3. ERb

ERb позволяет встраивать код Ruby в шаблоны. Формат фикстур YAML предварительно обрабатывается с помощью ERb при загрузке фикстур. Это позволяет использовать Ruby для помощи в генерации некоторых образцов данных. Например, следующий код сгенерирует тысячу пользователей:

<% 1000.times do |n| %>
user_<%= n %>:
  username: <%= "user#{n}" %>
  email: <%= "user#{n}@example.com" %>
<% end %>

3.2.4. Фикстуры в действии

Rails по умолчанию автоматически загружает все фикстуры из директории test/fixtures для ваших тестов моделей и контроллеров. Загрузка состоит из трех этапов:

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

Чтобы убрать существующие данные из базы, Rails пытается отключить триггеры ссылочной целостности (такие как внешние ключи и проверки ограничений). Если вы получаете надоедливые ошибки доступа при запуске тестов, убедитесь, что у пользователя базы данных есть привилегия отключать эти триггеры в тестовой среде. (В PostgreSQL только суперпользователи могут отключать все триггеры. Подробнее о разрешениях PostgreSQL читайте здесь)

3.2.5. Фикстуры это объекты Active Record

Фикстуры являются экземплярами Active Record. Как упоминалось в этапе №3 выше, Вы можете обращаться к объекту напрямую, поскольку он автоматически доступен как метод, область видимости которого локальна для тестового случая. Например:

# это возвратит объект User для фикстуры с именем david
users(:david)

# это возвратит свойство для david, названное id
users(:david).id

# он имеет доступ к методам, доступным для класса User
david = users(:david)
david.call(david.partner)

Чтобы получить несколько фикстур за раз, вы можете передать список имен фикстур. Например:

# это возвратит массив, содержаший фикстуры david и steve
users(:david, :steve)

4. Тестирование моделей

Тесты моделей используются для тестирования различных моделей вашего приложения.

Тесты моделей Rails хранятся в директории test/models directory. Rails предоставляет генератор для создания скелета теста модели.

$ bin/rails generate test_unit:model article title:string body:text
create  test/models/article_test.rb
create  test/fixtures/articles.yml

У тестов модели нет своего собственного суперкласса, такого как ActionMailer::TestCase, вместо этого они наследуются от ActiveSupport::TestCase.

5. Системное тестирование

Системные тесты позволяют тестировать взаимодействие пользователя с вашим приложением, запускать тесты либо в реальном, либо в headless браузере. Системные тесты используют Capybara под капотом.

Для создания системных тестов Rails используют директорию приложения test/system. Rails предоставляет генератор для создания скелета системного теста.

$ bin/rails generate system_test users
      invoke test_unit
      create test/system/users_test.rb

Вот как выглядит свежесгенерированный системный тест:

require "application_system_test_case"

class UsersTest < ApplicationSystemTestCase
  # test "visiting the index" do
  #   visit users_url
  #
  #   assert_selector "h1", text: "Users"
  # end
end

По умолчанию системные тесты запускаются с помощью драйвера Selenium с использованием браузера Chrome и разрешения экрана 1400x1400. Следующий раздел объясняет, как изменить настройки по умолчанию.

5.1. Изменение настроек по умолчанию

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

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

Если хотите изменить настройки по умолчанию, надо изменить то, с помощью чего "запускаются" ваши системные тесты. Скажем, вы хотите изменить драйвер с Selenium на Poltergeist. Сначала добавьте гем poltergeist в свой Gemfile. Затем сделайте в своем файле application_system_test_case.rb следующее:

require "test_helper"
require "capybara/poltergeist"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :poltergeist
end

Имя драйвера — обязательный аргумент для driven_by. Опциональные аргументы, который можно передать в driven_by это :using для браузера (используется только в Selenium), :screen_size, чтобы изменить размер экрана для скриншотов, и :options, которые могут использоваться для установки опций, поддерживаемых драйвером.

require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :firefox
end

Если необходимо использовать headless-браузер, можно использовать Headless Chrome или Headless Firefox, добавив headless_chrome или headless_firefox в аргумент :using.

require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
  driven_by :selenium, using: :headless_chrome
end

Если вашей конфигурации для Capybara требуется больше настроек, чем предоставлено Rails, то эту дополнительную конфигурацию можно поместить в файл application_system_test_case.rb.

За дополнительными настройками обратитесь к документации Capybara.

5.2. Хелпер для скриншотов

ScreenshotHelper - это хелпер, разработанный для захвата скриншотов ваших тестов. Это полезно для просмотра браузера в момент, когда упал тест, или для просмотра скриншотов для отладки.

Предоставляются два метода: take_screenshot и take_failed_screenshot. take_failed_screenshot автоматически включается в after_teardown внутри Rails.

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

5.3. Реализация системного теста

Теперь мы собираемся добавить системный тест в наше приложение блога. Мы продемонстрируем написание системного теста для посещения индексной страницы и для создания статьи в блоге.

При использовании генератора скаффолда, автоматически создавался скелет системного теста. Если не использовать генератор скаффолда, нужно начать с создания скелета системного теста.

$ bin/rails generate system_test articles

Это создаст файл теста. В результате выполнения предыдущей команды вы увидите:

      invoke  test_unit
      create    test/system/articles_test.rb

Теперь откройте этот файл и напишите первой утверждение:

require "application_system_test_case"

class ArticlesTest < ApplicationSystemTestCase
  test "viewing the index" do
    visit articles_path
    assert_selector "h1", text: "Articles"
  end
end

Тест должен увидеть, что на индексной странице статей есть h1 и пройти.

Запустите системные тесты.

bin/rails test:system

По умолчанию, выполнение bin/rails test не будет запускать ваши системные тесты. Убедитесь, что вы выполняете bin/rails test:system, чтобы на самом деле запустить их.

5.3.1. Системный тест для создания статей

Теперь давайте протестируем процесс создания новой статьи в нашем блоге.

test "creating an article" do
  visit articles_path

  click_on "New Article"

  fill_in "Title", with: "Creating an Article"
  fill_in "Body", with: "Created this article successfully!"

  click_on "Create Article"

  assert_text "Creating an Article"
end

Первый шаг — это вызов visit articles_path. Это приведет тест на индексную страницу статей.

Затем click_on "New Article" найдет кнопку "New Article" на индексной странице. Это перенаправит браузер на /articles/new.

Затем браузер заполнит title и body статьи представленным текстом. Как только поля будут заполнены, будет нажата "Create Article", что отправит запрос POST для создания новой статьи в базе данных.

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

5.3.2. Что дальше?

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

6. Интеграционное тестирование

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

Для создания интеграционных тестов Rails используется директория 'test/integration' нашего приложения. Rails предоставляет нам генератор для создания скелета интеграционного теста.

$ bin/rails generate integration_test user_flows
      exists  test/integration/
      create  test/integration/user_flows_test.rb

Вот как выглядит свежесгенерированный интеграционный тест:

require 'test_helper'

class UserFlowsTest < ActionDispatch::IntegrationTest
  # test "the truth" do
  #   assert true
  # end
end

Здесь тест наследуется от ActionController::IntegrationTest. Это делает доступным несколько дополнительных хелперов для использования в наших интеграционных тестах.

6.1. Хелперы, доступные для интеграционных тестов

В дополнение к стандартным хелперам тестирования, наследование от ActionDispatch::IntegrationTest дает несколько дополнительных хелперов для написания интеграционных тестов. Давайте для краткости представим три категории хелперов.

Для работы с runner-ом интеграционных тестов, смотрите ActionDispatch::Integration::Runner.

Для выполнения запросов у нас есть ActionDispatch::Integration::RequestHelpers.

Если хотим изменить сессию или состояние вашего интеграционного теста, нам поможет ActionDispatch::Integration::Session.

6.2. Реализация интеграционного теста

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

Начнем с генерации скелета нашего интеграционного теста:

$ bin/rails generate integration_test blog_flow

Он должен создать файл для размещения теста, и в результате предыдущей команды мы должны увидеть:

      invoke  test_unit
      create    test/integration/blog_flow_test.rb

Теперь откроем этот файл и напишем наше первое утверждение:

require 'test_helper'

class BlogFlowTest < ActionDispatch::IntegrationTest
  test "can see the welcome page" do
    get "/"
    assert_select "h1", "Welcome#index"
  end
end

Мы рассмотрим assert_select для запрашивания результирующего HTML запроса в разделе "Тестирование вьюх" ниже. Он используется для тестирования отклика на наш запрос, убеждаясь в наличии ключевых элементов HTML и их содержимого.

При посещении корневого пути мы должны увидеть welcome/index.html.erb, отрендеренную для представления. Таким образом, это утверждение должно пройти.

6.2.1. Создание интеграции статей

Как насчет тестирования возможности создавать новую статью в нашем блоге и просматривать полученную статью.

test "can create an article" do
  get "/articles/new"
  assert_response :success

  post "/articles",
    params: { article: { title: "can create", body: "article successfully." } }
  assert_response :redirect
  follow_redirect!
  assert_response :success
  assert_select "p", "Title:\n  can create"
end

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

Мы начинаем с вызова экшна :new контроллера Articles. Этот запрос должен быть успешным.

После этого мы делаем запрос post к экшну :create нашего контроллера Articles:

post "/articles",
  params: { article: { title: "can create", body: "article successfully." } }
assert_response :redirect
follow_redirect!

Следующие две строчки — это обработка редиректа, который мы настроили при создании новой статьи.

Не забывайте вызвать follow_redirect! Если планируете сделать последовательные запросы после выполнения редиректа.

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

6.2.2. Идем дальше

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

7. Функциональные тесты для ваших контроллеров

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

7.1. Что включать в функциональные тесты

Следует протестировать такие вещи, как:

  • был ли веб-запрос успешным?
  • был ли пользователь перенаправлен на правильную страницу?
  • был ли пользователь успешно аутентифицирован?
  • был ли правильный объект сохранен в шаблон отклика?
  • было ли подходящее сообщение отражено для пользователя во вьюхе

Самым простым способом увидеть функциональные тесты в действии является генерация контроллера с помощью генератора скаффолда:

$ bin/rails generate scaffold_controller article title:string body:text
...
create  app/controllers/articles_controller.rb
...
invoke  test_unit
create    test/controllers/articles_controller_test.rb
...

Это сгенерирует код контроллера и тестов для ресурса Article. Можете взглянуть на файл articles_controller_test.rb в директории test/controllers.

Если у вас уже есть контроллер и вы просто хотите сгенерировать код теста скаффолда для каждого из семи экшнов по умолчанию, можете использовать следующую команду:

$ bin/rails generate test_unit:scaffold article
...
invoke  test_unit
create    test/controllers/articles_controller_test.rb
...

Давайте взглянем на один такой тест, test_should_get_index из файла articles_controller_test.rb.

# articles_controller_test.rb
class ArticlesControllerTest < ActionDispatch::IntegrationTest
  test "should get index" do
    get articles_url
    assert_response :success
  end
end

В тесте test_should_get_index, Rails имитирует запрос к экшну index, убеждается, что запрос был успешным, а также обеспечивает, что генерируется правильное тело ответа.

Метод get стартует веб-запрос и заполняет результаты в @response. Он может принимать до 6 аргументов:

  • URI экшна контроллера, к которому обращаетесь. Он может быть в форме строки или хелпера маршрута (например, articles_url).
  • params: опция с хэшем параметров запроса для передачи в экшн (например, параметры строки запроса или переменные для модели article).
  • headers: для установки заголовков, которые будут переданы с запросом.
  • env: для настройки среды запроса по необходимости.
  • xhr: был ли запрос Ajax или нет. Можно установить true для пометки, что запрос является Ajax.
  • as: для кодировки запроса другим типом запроса. По умолчанию поддерживает :json.

Все эти аргументы с ключевым словом опциональны.

Пример: Вызов экшна :show, передача id, равного 12, как params, и установка заголовка HTTP_REFERER:

get article_url, params: { id: 12 }, headers: { "HTTP_REFERER" => "http://example.com/home" }

Другой пример: Вызов экшна :update, передача id, равного 12, как params, как запрос Ajax.

patch article_url, params: { id: 12 }, xhr: true

Если попытаетесь запустить тест test_should_create_article из articles_controller_test.rb, он провалится из-за недавно добавленной валидации на уровне модели, и это правильно.

Давайте изменим тест test_should_create_article в articles_controller_test.rb так, чтобы все наши тесты проходили:

test "should create article" do
  assert_difference('Article.count') do
    post articles_url, params: { article: { body: 'Rails is awesome!', title: 'Hello Rails' } }
  end

  assert_redirected_to article_path(Article.last)
end

Теперь можете попробовать запустить все тесты, и они должны пройти.

Если выполняли шаги в разделе про простую аутентификацию, то необходимо добавить следующее в блок setup, чтобы все тесты проходили:

request.headers['Authorization'] = ActionController::HttpAuthentication::Basic.
  encode_credentials('dhh', 'secret')

7.2. Доступные типы запросов для функциональных тестов

Если вы знакомы с протоколом HTTP, то знаете, что get это тип запроса. Имеется 6 типов запросов, поддерживаемых в функциональных тестах Rails:

  • get
  • post
  • patch
  • put
  • head
  • delete

У всех типов запросов есть эквивалентные методы, которые можно использовать. В обычном приложении C.R.U.D. вы чаще будете использовать get, post, put и delete.

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

7.3. Тестирование запросов XHR (AJAX)

Чтобы протестировать запросы AJAX, можно указать опцию xhr: true в методах get, post, patch, put и delete. Например:

test "ajax request" do
  article = articles(:one)
  get article_url(article), xhr: true

  assert_equal 'hello world', @response.body
  assert_equal "text/javascript", @response.content_type
end

7.4. Три Хэша Апокалипсиса (The Three Hashes of the Apocalypse)

После того, как запрос был сделан и обработан, у вас будет 3 объекта Hash, готовых для использования:

  • cookies - Любые установленные куки
  • flash - Любые объекты, находящиеся во flash
  • session - Любый объекты, находящиеся в переменных сессии

Как и в случае с обычными объектами Hash, можете получать доступ к значениям, указав ключ в строке. Также можете указать его именем символа. Например:

flash["gordon"]               flash[:gordon]
session["shmession"]          session[:shmession]
cookies["are_good_for_u"]     cookies[:are_good_for_u]

7.5. Доступные переменные экземпляра

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

  • @controller - Контроллер, обрабатывающий запрос
  • @request - Объект запроса
  • @response - Объект отклика
class ArticlesControllerTest < ActionDispatch::IntegrationTest
  test "should get index" do
    get articles_url

    assert_equal "index", @controller.action_name
    assert_equal "application/x-www-form-urlencoded", @request.media_type
    assert_match "Articles", @response.body
  end
end

7.6. Установка заголовков и переменных CGI

Заголовки HTTP и переменные CGI могут быть переданы как заголовки:

# устанавливаем заголовок HTTP
get articles_url, headers: { "Content-Type": "text/plain" } # имитировать запрос с пользовательским заголовком

# устанавливаем переменную CGI
get articles_url, headers: { "HTTP_REFERER": "http://example.com/home" } # имитировать запрос с пользовательской env переменной

7.7. Тестирование сообщений flash

Как помните, одним из трех хэшей был flash.

Мы хотим добавить сообщение flash в наше приложение блога, всякий раз, когда кто-то успешно создает новый объект Article.

Давайте начнем с добавления этого утверждения в наш тест test_should_create_article:

test_should_create_article do
  assert_difference('Article.count') do
    post article_url, params: { article: { title: 'Some title' } }
  end

  assert_redirected_to article_path(Article.last)
  assert_equal 'Article was successfully created.', flash[:notice]
end

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

$ bin/rails test test/controllers/articles_controller_test.rb -n test_should_create_article
Run options: -n test_should_create_article --seed 32266

# Running:

F

Finished in 0.114870s, 8.7055 runs/s, 34.8220 assertions/s.

  1) Failure:
ArticlesControllerTest#test_should_create_article [/test/controllers/articles_controller_test.rb:16]:
--- expected
+++ actual
@@ -1 +1 @@
-"Article was successfully created."
+nil

1 runs, 4 assertions, 1 failures, 0 errors, 0 skips

Теперь давайте реализуем сообщение flash в нашем контроллере. Наш экшн :create теперь должен выглядеть так:

def create
  @article = Article.new(article_params)

  if @article.save
    flash[:notice] = 'Article was successfully created.'
    redirect_to @article
  else
    render 'new'
  end
end

Если теперь запустить наши тесты, мы увидим, что он проходит:

$ bin/rails test test/controllers/articles_controller_test.rb -n test_should_create_article
Run options: -n test_should_create_article --seed 18981

# Running:

.

Finished in 0.081972s, 12.1993 runs/s, 48.7972 assertions/s.

1 runs, 4 assertions, 0 failures, 0 errors, 0 skips

7.8. Обобщение изложенного

С этого момента в нашем контроллере Articles тестируются экшны :index, :new и :create. Но как насчет работы с существующими данными?

Давайте напишем тест для экшна :show:

test "should show article" do
  article = articles(:one)
  get article_url(article)
  assert_response :success
end

Как помните из нашего обсуждения фикстур, что метод articles() дает нам доступ к нашим фикстурам Articles.

Как насчет удаления существующего объекта Article?

test "should destroy article" do
  article = articles(:one)
  assert_difference('Article.count', -1) do
    delete article_url(article)
  end

  assert_redirected_to articles_path
end

Также можно добавить тест для обновления существующего объекта Article.

test "should update article" do
  article = articles(:one)

  patch article_url(article), params: { article: { title: "updated" } }

  assert_redirected_to article_path(article)
  # Перезагрузим связь, чтобы извлечь обновленные данные и убедиться, что заголовок обновлен.
  article.reload
  assert_equal "updated", article.title
end

Отметьте, что у нас имеется некоторое дублирование в этих трех тестах, они все получают доступ к одним и тем же данным фикстуры Article. Можно убрать повторения с помощью методов setup и teardown, предоставленных ActiveSupport::Callbacks.

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

require 'test_helper'

class ArticlesControllerTest < ActionDispatch::IntegrationTest
  # вызывается перед каждым отдельным тестом
  setup do
    @article = articles(:one)
  end

  # вызывается после каждого отдельного теста
  teardown do
    # когда контроллер использует кэш, это может быть хорошей идеей сбросить его затем
    Rails.cache.clear
  end

  test "should show article" do
    # переиспользуем инстанс переменную @article из setup
    get article_url(@article)}
    assert_response :success
  end

  test "should destroy article" do
    assert_difference('Article.count', -1) do
      delete article_url(@article)
    end

    assert_redirected_to articles_path
  end

  test "should update article" do
    patch article_url(@article), params: { article: { title: "updated" } }

    assert_redirected_to article_path(@article)
    # Перезагрузим связь, чтобы извлечь обновленные данные и убедиться, что заголовок обновлен.
    article.reload
    assert_equal "updated", article.title
  end
end

Подобно другим колбэкам Rails, методы setup и teardown можно использовать, передав блок, lambda или имя метода символом для вызова.

7.9. Тестовые хелперы

Чтобы избежать дублирования кода, можно добавлять собственные тестовые хелперы. Хорошим примером может быть хелпер входа в систему:

# test/test_helper.rb

module SignInHelper
  def sign_in_as(user)
    post sign_in_url(email: user.email, password: user.password)
  end
end

class ActionDispatch::IntegrationTest
  include SignInHelper
end

require 'test_helper'

class ProfileControllerTest < ActionDispatch::IntegrationTest

  test "should show profile" do
    # теперь хелпер можно повторно использовать в любом тестовом случае контроллера
    sign_in_as users(:david)

    get profile_url
    assert_response :success
  end
end

8. Тестирование маршрутов

Как и все другое в вашем приложении Rails, ваши маршруты можно тестировать. Тесты маршрутов находятся в test/controllers/ или являются частью тестов контроллера.

Если в вашем приложении сложные маршруты, Rails предоставляет ряд полезных хелперов для их тестирования.

Подробности о тестировании маршрутов доступны в Rails, обратитесь к документации API для ActionDispatch::Assertions::RoutingAssertions.

9. Тестирование вьюх

Тестирование отклика на ваш запрос с помощью подтверждения наличия ключевых элементов HTML и их содержимого, это хороший способ протестировать вьюхи вашего приложения. Как и тесты маршрутов, тесты вьюх находятся в test/controllers/ или являются частью тестов контроллера. Метод assert_select позволяет осуществить выборку элементов HTML отклика с помощью простого, но мощного синтаксиса.

Имеется две формы assert_select:

assert_select(selector, [equality], [message]) обеспечивает, что условие equality выполняется для выбранных через selector элементов, selector может быть выражением селектора CSS (String) или выражением с заменяемыми значениями.

assert_select(element, selector, [equality], [message]) обеспечивает, что условие equality выполняется для всех элементов, выбранных через selector начиная с element (экземпляра Nokogiri::XML::Node или Nokogiri::XML::NodeSet) и его потомков.

Например, можете проверить содержимое в элементе title вашего ответа с помощью:

assert_select 'title', "Welcome to Rails Testing Guide"

Также можно использовать вложенные блоки assert_select для углубленного исследования.

В следующем примере, внутренний assert_select для li.menu_item запускается для полной коллекции элементов, выбранных во внешнем блоке:

assert_select 'ul.navigation' do
  assert_select 'li.menu_item'
end

Коллекция выбранных элементов может быть перебрана, таким образом assert_select может быть вызван отдельно для каждого элемента.

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

assert_select "ol" do |elements|
  elements.each do |element|
    assert_select element, "li", 4
  end
end

assert_select "ol" do
  assert_select "li", 8
end

Это утверждение достаточно мощное. Для более продвинутого использования обратитесь к его документации.

9.1. Дополнительные утверждения, основанные на вьюхе

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

Утверждение Назначение
assert_select_email Позволяет сделать утверждение относительно тела e-mail.
assert_select_encoded Позволяет сделать утверждение относительно закодированного HTML. Он делает это декодируя содержимое каждого элемента и затем вызывая блок со всеми декодированными элементами.
css_select(selector) или css_select(element, selector) Возвращают массив всех элементов, выбранных через selector. Во втором варианте сначала проверяется соответствие базовому element, а затем пытается применить соответствие выражению selector на каждом из его детей. Если нет соответствий, оба варианта возвращают пустой массив.

Вот пример использования assert_select_email:

assert_select_email do
  assert_select 'small', 'Please click the "Unsubscribe" link if you want to opt-out.'
end

10. Тестирование хелперов

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

Чтобы протестировать хелперы, нужно проверить, что результат метода хелпера соответствует тому, что вы ожидаете. Тесты, относящиеся к хелперам, расположены в директории test/helpers.

Допустим, у нас имеется следующий хелпер:

module UserHelper
  def link_to_user(user)
    link_to "#{user.first_name} #{user.last_name}", user
  end
end

Мы можем протестировать результат этого метода хелпера следующим образом:

class UserHelperTest < ActionView::TestCase
  test "should return the user's full name" do
    user = users(:david)

    assert_dom_equal %{<a href="/user/#{user.id}">David Heinemeier Hansson</a>}, link_to_user(user)
  end
end

Более того, так как этот класс теста расширяет ActionView::TestCase, у вас есть доступ к методам хелпера Rails, таким как link_to или pluralize.

11. Тестирование почтовых рассыльщиков

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

11.1. Держим почтовик под контролем

Ваши классы рассыльщика - как и любая другая часть вашего приложения на Rails - должны быть протестированы, что они работают так, как ожидается.

Тестировать классы рассыльщика нужно, чтобы быть уверенным в том, что:

  • электронные письма обрабатываются (создаются и отсылаются)
  • содержимое письма правильное (тема, получатель, тело и т.д.)
  • правильные письма отправляются в нужный момент
11.1.1. Со всех сторон

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

11.2. Юнит-тестирование

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

11.2.1. Реванш фикстур

Для целей юнит-тестирования рассыльщика фикстуры используются для предоставления примера, как результат должен выглядеть. Так как это примеры электронных писем, а не данные Active Record, как в других фикстурах, они должны храниться в своей поддиректории отдельно от других фикстур. Имя директории в test/fixtures полностью соответствует имени рассыльщика. Таким образом, для рассыльщика с именем UserMailer фикстуры должны располагаться в директории test/fixtures/user_mailer.

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

11.2.2. Простой тестовый случай

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

require 'test_helper'

class UserMailerTest < ActionMailer::TestCase
  test "invite" do
    # Создайте email и сохраните его для будущих утверждений
    email = UserMailer.create_invite('me@example.com',
                                     'friend@example.com', Time.now)

    # Отправить письмо, затем проверить, что оно попало в очередь
    assert_emails 1 do
      email.deliver_now
    end

    # Проверить тело отправленного письма, что оно содержит то, что мы ожидаем
    assert_equal ['me@example.com'], email.from
    assert_equal ['friend@example.com'], email.to
    assert_equal 'You have been invited by me@example.com', email.subject
    assert_equal read_fixture('invite').join, email.body.to_s
  end
end

В тесте мы посылаем письмо и сохраняем возвращенный объект в переменной email. Затем мы убеждаемся, что оно было послано (первое утверждение), затем, вот второй порции утверждений, мы убеждаемся, что email содержит в точности то, что мы ожидаем. Хелпер read_fixture используется для считывания содержимого из этого файла.

email.body.to_s существует только когда присутствует одна часть (HTML или text). Если рассыльщик представляет обе, можно протестировать фикстуру для определенной части с помощью email.text_part.body.to_s или email.html_part.body.to_s.

Вот содержимое фикстуры invite:

Hi friend@example.com,

You have been invited.

Cheers!

Сейчас самое время понять немного больше о написании тестов для ваших рассыльщиков. Строчка ActionMailer::Base.delivery_method = :test в config/environments/test.rb устанавливает метод доставки в тестовом режиме, таким образом, письмо не будет фактически доставлено (полезно во избежание спама для ваших пользователей во время тестирования), но вместо этого оно будет присоединено к массиву (ActionMailer::Base.deliveries).

Массив ActionMailer::Base.deliveries перезагружается автоматически только в тестах ActionMailer::TestCase и ActionDispatch::IntegrationTest. Если необходим чистый массив вне этих тестовых случаев, его можно перезагрузить вручную с помощью ActionMailer::Base.deliveries.clear

11.3. Функциональное тестирование

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

require 'test_helper'

class UserControllerTest < ActionDispatch::IntegrationTest
  test "invite friend" do
    assert_difference 'ActionMailer::Base.deliveries.size', +1 do
      post invite_friend_url, params: { email: 'friend@example.com' }
    end
    invite_email = ActionMailer::Base.deliveries.last

    assert_equal "You have been invited by me@example.com", invite_email.subject
    assert_equal 'friend@example.com', invite_email.to[0]
    assert_match(/Hi friend@example\.com/, invite_email.body.to_s)
  end
end

12. Тестирование заданий

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

12.1. Простой тестовый случай

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

require 'test_helper'

class BillingJobTest < ActiveJob::TestCase
  test 'that account is charged' do
    BillingJob.perform_now(account, product)
    assert account.reload.charged_for?(product)
  end
end

Это очень простой пример, он только проверяет, что задание делает работу так, как ожидается.

По умолчанию ActiveJob::TestCase устанавливает адаптер очереди :test, чтобы ваши задания выполнялись сразу. Это также позволяет убедиться, что все ранее выполненные и поставленные в очередь задания будут очищены до запуска любого теста, таким образом гарантируется, что в рамках каждого теста нет ранее запущенных заданий.

12.2. Пользовательские утверждения и тестирование заданий внутри других компонент

Active Job поставляется с набором пользовательских утверждений, которые могут быть использованы для уменьшения уровня детализации тестов. Полный список утверждений смотрите в документации API для ActiveJob::TestHelper.

Хорошей практикой бывает убедиться, что ваши задания были поставлены в очередь или выполнены, там, где вы их вызываете (например, внутри контроллера). Именно тут пользовательские утверждения, предоставленные Active Job, особенно полезны. Например, в модели:

require 'test_helper'

class ProductTest < ActiveJob::TestCase
  test 'billing job scheduling' do
    assert_enqueued_with(job: BillingJob) do
      product.charge(account)
    end
  end
end

13. Дополнительные ресурсы по тестированию

13.1. Тестирование кода, зависимого от времени

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

Вот пример использования хелпера travel_to:

# Допустим, что пользователю можно сделать подарок через месяц после регистрации.
user = User.create(name: 'Gaurish', activation_date: Date.new(2004, 10, 24))
assert_not user.applicable_for_gifting?
travel_to Date.new(2004, 11, 24) do
  assert_equal Date.new(2004, 10, 24), user.activation_date # внутри блока `travel_to` `Date.current` имитируется
  assert user.applicable_for_gifting?
end
assert_equal Date.new(2004, 10, 24), user.activation_date # Изменение было видно только внутри блока `travel_to`.

Обратитесь к документации ActiveSupport::Testing::TimeHelpers API за более подробной информацией о доступных хелперах времени.