:aggregate_failures
Обычно Rspec останавливается на первой ошибке и не показывает остальные, если мы хотим показать остальные ошибки, тогда в примере (‘it’) нужно добавить тег ‘:aggregate_failures’
it 'successfully saves the expense in the DB', :aggregate_failures do
Code language: Ruby (ruby)
Случайный порядок тестов
По дефолту RSpec использует случайный порядок выполнения тестов. Эту строку можно найти в блоке ‘RSpec.configure’ ‘config.order = :random‘
Опция «—bisect» позволяет систематически запускать различные части вашего пакета, пока не найдет наименьший набор, который вызывает сбой. Так же можно настраивать порядок выполнения примеров и групп
Поиск файлов в которые включен другой файл
grep config/sequel -r . --exclude-dir=.git
Запуск каждого spec файла
(for f in `find spec -iname '*_spec.rb'`; do
echo "$f:"
bundle exec rspec $f -fp || exit 1
done)
Code language: Bash (bash)
before(:all)
и before(:suite)
Когда в блоке RSpec.configure определен элемент before (: all), он вызывается перед каждой группой примеров верхнего уровня, в то время как блок кода before (: suite) вызывается только один раз.
RSpec.configure do |config|
config.before(:all) { puts 'Before :all' }
config.after(:all) { puts 'After :all' }
config.before(:suite) { puts 'Before :suite' }
config.after(:suite) { puts 'After :suite' }
end
describe 'spec1' do
example 'spec1' do
puts 'spec1'
end
end
describe 'spec2' do
example 'spec2' do
puts 'spec2'
end
end
Code language: Ruby (ruby)
Результат:
Before :suite
Before :all
spec1
After :all
Before :all
spec2
After :all
After :suite
Code language: CSS (css)
Ошибка при миграции базы данных в Sinatra An error occurred in a before(:suite)
hook.
An error occurred in a `before(:suite)` hook.
Failure/Error: Sequel::Migrator.run(DB, 'db/migrations')
ArgumentError:
wrong number of arguments (given 2, expected 1)
# <internal:kernel>:48:in `clone'
# ./spec/support/db.rb:5:in `block (2 levels) in <top (required)>'
Code language: PHP (php)
Что бы исправить эту ошибку мне пришлось понизить версию руби с 3.0.0 до ruby 2.6.6
Хуки
- before
- after
- around
Хуки могут быть запущены для каждого примера (:example), однажды для каждого контекста (:context) или глобально для всего набора (:suite)
Поделиться общей логикой с другими примерами используя хуки
https://relishapp.com/rspec/rspec-core/docs/example-groups/shared-examples
Мы можем установить общую логику на уровне файла конфигурации
# Запуститься после загрузки тестов и перед первым примером
RSpec.configure do |c|
c.before(:suite) do
Sequel.extension :migration
Sequel::Migrator.run(DB, 'db/migrations')
DB[:expenses].truncate
end
end
# Еще пример
require 'fileutils'
RSpec.configure do |config|
config.before(:suite) do
# Remove leftover temporary files
FileUtils.rm_rf('tmp')
end
end
# Пример
# Избежание засорения каждой спецификации, зависящей от базы данных,
# этой логикой транзакции.
RSpec.configure do |c|
c.around(:example, :db) do |example|
DB.transaction(rollback: :always) { example.run }
end
end
Code language: Ruby (ruby)
Нужно осторожно использовать :context хук. Так как можно забыть очистить результаты выполнения хука и оставить базу данных в неправильном состоянии, что может привести к прохождению теста в тестовой среде, но в продакшене работать не будет.
RSpec.configure — единственное место, где разрешены хуки набора (:suite) Они «живут» независимо от других «примеров» или «групп»
around хук поддерживается только в примере (:example) теста
Поделиться общей логикой используя shared_context, include_context (shared_examples, include_examples)
По договоренности общие примеры должны находиться в spec/support. Так же если не следовать этому соглашению RSpec может решить что это обычный файл спецификации и покажет предупреждающие сообщения
it_behaves_like "name" #включить примеры во вложенный контекст
shared_examples "a collection" do
context "initialized with 3 items" do
it "says it has three items" do
# ...
end
end
end
describe Array do
it_behaves_like "a collection"
include_examples "a collection"
end
Code language: Ruby (ruby)
Nesting (вложенность)
:aggregate_failures — показывает все тесты с ошибками. Дополнительные настройки можно установить с помощью блока. На пример в блоке ниже можно добавить if выражение, которое поможет пропускать некоторые примеры
RSpec.configure do |config|
config.define_derived_metadata do |meta|
# Безоговорочно устанавливает флаг;
# не позволяет примерам отказаться
meta[:aggregate_failures] = true
end
end
Code language: Ruby (ruby)
Запуск определенных примеров по тегу
Бывает полезно когда мы работам над определенным функционалом. Что бы не запускать все тесты, запустим только тесты у которых есть определенный тег
( filter_run_including синоним filter_run )
RSpec.configure do |config|
config.filter_run_excluding :jruby_only unless RUBY_PLATFORM == 'python'
end
Code language: Ruby (ruby)
Но легче это сделать с помощью командной строки
прим. rspec —tag cars Если использовать знак ~ тесты с тегом cars будут исключены.
Другими словами это 2 основных способа конфигурации RSpec
Доступные команды для RSpec. rspec —help
Полный список команд можно получить с помощью запроса rspec —help в командной строке
Что бы добавить byebug можно воспользоваться командой rspec -rbyebug
Фильтры при запуске тестов
rspec some/path/some_test_spec.rb:12
Запуск определенного примера или группы
—only-failures только неудачные тесты
—next-failure в случае если мы хотим исправлять тесты по одному
—example ‘описание теста’ можно использовать вместо указания номера строки
—tag запуск тестов по тегу
Есть фильтр информации которая выводиться во время и после теста. Для этого есть различные флаги
- -f Выбор формата
- -o вывод в файл
- -b, —backtrace Детальная информация
- -p Включить профилирование примеров и список медленных тестов
- -dry-run
- -w Включить только ruby предупреждения
Разные форматы вывода информации
Например, можно показать информацию об ошибке не останавливая тест. Для этого можно использовать gem https://rubygems.org/gems/rspec-print_failures_eagerly/versions/1.0.0
Rspec сравнители (сопоставители).
Сравнители для значений с плавающей запятой.
> radius = 3
=> 3
> area_of_circle = radius * radius * Math::PI
=> 28.2743338823081
> area_of_circle == 28.2743338823081
=> false
Code language: PHP (php)
В таких случаях нужно использовать
area_of_circle.should be_within(0.1).of(28.3)
Code language: CSS (css)
Это не сравнение точного числа, а применение диапазона
Например, для be_within(0.0001).of(0.3) будет диапазон 0.2999 and 0.3001
Можно задать диапазон в виде процентов.
max_power = 110
expect(max_power).to be_within(25).percent_of(100)
Задать диапазон
expect(max_power).to be_between(75, 125)
https://relishapp.com/rspec/rspec-expectations/v/2-8/docs/built-in-matchers/be-within-matcher
Сопоставители (сравнители)
- Примитивные сравнители
- Сравнители высокого порядка
- Блок сравнители
Примитивные сравнители
Должно быть именно это значение
expect(one).to eq(two)
expect(1).to eq(1.0) #true
Code language: Ruby (ruby)
Должен быть именно этот объект (псевдоним be(x))
expect(one).to equal(two)
Code language: Ruby (ruby)
eql? что-то среднее между примерами приведенными выше
a.eql?(b) имеется такое же значение, однако не конвертирует типы данных. По этому выражение ниже будет верным:
expect(1).not_to eql(1) # false
expect(1).not_to eql(1.0) # true
Code language: Ruby (ruby)
Передача значения в сравнитель
squares = 1.upto(10).map { |i| i * i }
expect(squares).to include(a_value > 13)
Code language: Ruby (ruby)
Предикаты — отвечают ложно/верно
hash = { name: 'Ford', power: 170 }
expect(hash).to have_key(:power)
# Прим 2
expect([]).to be_empty
Code language: PHP (php)
Пример когда будут полезны предикаты
expect(user.admin?).to eq(true)
# Тест не пройдет
Code language: CSS (css)
Нужно использовать это:
expect(user).to be_admin или
expect(user).to be_an_admin
Code language: CSS (css)
Сложные сопоставления.
Иногда сложно написать правильный сопоставитель для теста. Тогда можно воспользоваться блоками
expect(5).to satisfy { |number| number.odd? }
expect([4, 5, 6]).to include(an_object_satisfying(&:even?))
Code language: PHP (php)
Сравнители высшего порядка
Для коллекций используются 6 основных сопоставителей:
- Include
- start_with и end_with
- all
- match
- contain_exactly Порядок не имеет значения, но должны быть все елементы
Include
expect('Hello World').to include('Wor')
expect([3, 4, 5]).to include(3)
Code language: PHP (php)
Нужно быть осторожным, например следующий пример провалиться, так как сравнитель ожидает найти массив [4, 5]. Однако тест бы прошел если бы значение expect было [3, [4, 5]]
# False
expecteds = [4, 5]
expect([3, 4, 5]).to include(expecteds)
# True
expecteds = [4, 5]
expect([3, 4, 5]).to include(*expecteds)
Code language: PHP (php)
Так же этот сравнитель доступен как:
- a_collection_including
- a_string_including
- a_hash_including
start_with и end_with
expect([3, 4, 5]).to start_with(3).and end_with(5)
expect('Hello World').to start_with('He').and end_with('ld')
Code language: JavaScript (javascript)
All
Единственный сопоставитель который не является глаголом. Только он один всегда принимает другой сопоставитель как аргумент.
cars = [1, 3, 5]
expect(cars).to all be_odd
Если нам нужно определить противоположное значение,мы можем воспользоваться методом ‘define_negated_matcher’
RSpec::Matchers.define_negated_matcher :be_non_empty, :be_empty
Code language: CSS (css)
match
Подходит для сравнения хешей и массивов. Мы можем применять разные сопоставители в ожидаемых значениях. Значения хеша или массива должны быть в том же порядке для прохождения теста
car = [
{ make: 'ford', price: '10000' }
]
expect(car).to match [
{ make: 'ford', price: a_value_between(5000, 15000) }
]
Code language: JavaScript (javascript)
contain_exactly
В отличие от match этот метод игнорирует порядок
Проверка атрибутов объекта
https://www.rubydoc.info/gems/rspec-expectations/RSpec%2FMatchers:have_attributes
Сопоставители блоков
raise и throw
raise_error
Может принимать несколько аргументов. raise_error без аргументов будет соответствовать любой ошибке.
Желательно точно описывать ошибку, так как часто при изменении кода тесты не находят ошибку, так как ошибка была описана слишком ‘обобщенно’.
Так же при описании теста мы можем сделать опечатку наш тест будет проходить, однако ничего не будет проверять.
Например, у нас есть метод cars_of в тесте мы написали car_of. Тест пройдет, хотя метод никогда не будет запускаться…
expect { car_of(user) }.not_to raise_error(MissingDataError)
Однако, в таком случае Rspec предупредит и предложит исправление теста not_to raise_error без аргументов
throw_symbol
Не совсем подходит для обычных ситуаций. Используется в ситуациях таких как ошибка программы.
expect { throw :found, 11 }.to throw_symbol(:found, a_value > 10)
Code language: CSS (css)
Вставка и Блоки
yield_control
Проверка. Ведет ли себя объект как блок. Можно определить сколько раз вставлять выражение в блок
yield_with_args
Для проверки с аргументов которые должен принимать блок
yield_with_no_args
Проверка блока без аргументов
yield_successive_arrs
expect { |block|
['cars', 'ford'].each_with_index(&block)
}.to yield_successive_args(
[/car/, 0], [a_string_starting_with('fo'), 1]
)
Code language: JavaScript (javascript)
Мутации
change
Изменения состояния. Например после отправки почты.
Еще более простой пример:
array = [3, 4, 5]
expect { array << 6 }.to change { array.size }
Code language: PHP (php)
В примере выше «change» это блок. Хранит начальные данные в значении «before», после выполнения блока мы сравниваем со значением ‘after’. Эти значения будут разными. Тест пройдет
Что бы конкретизировать изменения используются следующие методы
by, by_at_least, by_at_most
Можем проверить диапозон до и помле изменения. From и to
Для проверки, что значение не изменилось from
x=10
expect { }.not_to change { x }.from(10)
Вывод данных
expect { print 'OK' }.to output('OK').to_stdout
Code language: PHP (php)
Rspec позволяет создавать псевдонимы для сопоставителей
и создавать собственные сопоставители. Есть 2 подхода. Использовать DSL и создание класса. Лучше использовать DSL для обычных ситуаций, однако при написании библиотеки, лучше создать сопостовители с помощью классов.
Используйте проверочные двойники для более раннего выявления проблем. Использование instance_double вместо double позволяет избежать ошибки:
instance_double проверяет существует ли этот метод для этого класса, double не делает этого. По этому если мы изменим название метода наши тесты будут проходить, однако на сайте будет ошибка
ledger = instance_double('ExpenseTracker::Ledger')
allow(ledger).to receive(:record)
Code language: Ruby (ruby)
Есть несколько способов создать проверочные двойники:
- instance_double(‘SomeClass’)
- class_double(‘SomeClass’)
- object_double(some_object)
Test Double Customizing. Заглушки.
- Pure doubles (полностью фальшивые)
- Partial doubles (это настоящие объекты Ruby с добавленным фальшивым поведением)
- Verifying doubles (находятся посередине и имеют преимущества обоих с небольшими недостатками любого из них. Именно их мы используем чаще всего.)
Используйте проверочные двойники для более раннего выявления проблем. Использование instance_double вместо double позволяет избежать ошибки:
instance_double проверяет существует ли этот метод для этого класса, double не делает этого. По этому если мы изменим название метода наши тесты будут проходить, однако на сайте будет ошибка
ledger = instance_double('ExpenseTracker::Ledger')
allow(ledger).to receive(:record)
Code language: JavaScript (javascript)
Есть несколько способов создать проверочные двойники:
- instance_double(‘SomeClass’)
- class_double(‘SomeClass’)
- object_double(some_object)
Настройка ответов.
Если не предоставить информацию как это должно отвечать это просто вернет nil
allow(double).to receive(:a_message)
Code language: CSS (css)
Мы можем указать что конкретно мы хотим получить (вариантов много)
allow(double).to receive(:a_message).and_return(a_return_value)
Code language: CSS (css)
Так же
expect(some_existing_object).to receive(:a_message)
some_existing_object.a_message
#=> nil
Code language: Ruby (ruby)
Если мы хотим получить a_message нужно делать так:
expect(some_existing_object).to receive(:a_message).and_call_original
Code language: CSS (css)
Настройка нескольких ответов по порядку
allow(random).to receive(:rand).and_return(0.1, 0.2, 0.3)
>> random.rand => 0.1
>> random.rand => 0.2
>> random.rand => 0.3
>> random.rand => 0.3
>> random.rand => 0.3
Code language: PHP (php)
Вставка нескольких значений (блоки)
extractor = double('TwitterURLExtractor')
allow(extractor).to receive(:extract_urls_from_twitter_firehose)
.and_yield('https://rspec.info/', 93284234987)
.and_yield('https://github.com/', 43984523459)
.and_yield('https://pragprog.com/', 33745639845)
Code language: JavaScript (javascript)
Гибкая настройка сообщений об ошибках
allow(dbl).to receive(:msg).and_raise(AnExceptionClass)
allow(dbl).to receive(:msg).and_raise('an error message')
allow(dbl).to receive(:msg).and_raise(AnExceptionClass, 'with a message')
an_exception_instance = AnExceptionClass.new
allow(dbl).to receive(:msg).and_raise(an_exception_instance)
Code language: PHP (php)
Возврат к исходной реализации
# fake implementation for specific arguments: allow(File).to
receive(:read).with('/etc/passwd').and_raise('HAHA NOPE')
# fallback:
allow(File).to receive(:read).and_call_original
Code language: PHP (php)
Здесь мы использовали with(…) для ограничения значений параметров, к которым применяется эта заглушка.
Модификация возвращаемого значения
allow(CustomerService).to receive(:all).and_wrap_original do |original|
all_customers = original.call
all_customers.sort_by(&:id).take(10)
end
Code language: Ruby (ruby)
Этот метод может быть удобен для приемочных спецификаций, когда вы хотите протестировать реальный сервис. Если поставщик не предоставляет тестовый API, который возвращает только несколько записей, вы можете вызвать реальный API и самостоятельно сузить список записей. Работая только с подмножеством данных, ваши спецификации останутся быстрыми.
Настройка аргументов (константы) Setting-constraints
allow(PasswordHash).to receive(:hash_password)
.and_wrap_original do |original, cost_factor|
original.call(1)
end
Code language: Ruby (ruby)
Если заглушаемый метод принимает аргументы (например, cost_factor), RSpec передает их в качестве дополнительных параметров в ваш блок.
Можно предоставить блок, содержащий любое пользовательское поведение, которое вам нужно.
counter = 0
allow(weather_api).to receive(:temperature) do |zip_code|
counter = (counter + 1) % 4
counter.zero? ? raise(Timeout::Error) : 35.0
end
Code language: JavaScript (javascript)
Когда ваш код вызывает weather_api.temperature(some_zip_code), RSpec запускает этот блок и, в зависимости от того, сколько вызовов вы сделали, либо возвращает значение, либо вызывает исключение тайм-аута.
Установка ограничений
Вызов с правильными параметрами .with
expect(movie).to receive(:record_review).with('Great movie!')
expect(movie).to receive(:record_review).with(/Great/)
expect(movie).to receive(:record_review).with('Great movie!', 5)
Code language: JavaScript (javascript)
Hashes and Keyword Arguments
expect(box_office).to receive(:find_showtime)
.with(hash_including(movie: 'Jaws'))
Code language: Ruby (ruby)
Противоположность. .hash_excluding (Хеш не включает)
Можно передать только один параметер вместо всех
Ограничение на кол-во раз вызова
client = instance_double('NasdaqClient')
expect(client).to receive(:current_price)<strong>.thrice</strong>.and_raise(Timeout::Error)
stock_ticker = StockTicker.new(client)
100.times { stock_ticker.price('AAPL') }
Code language: Ruby (ruby)
Тут мы использовали слово .thrice , но для значений больше 3х, нужно использовать
exactly(n).times
expect(client).to receive(:current_price).exactly(4).times
Code language: CSS (css)
Минимум и максимум то же поддерживаются
expect(client).to receive(:current_price).at_least(3).times
expect(client).to receive(:current_price).at_most(10).times
Code language: CSS (css)
Порядок отправки сообщений двойникам
expect(greeter).to receive(:hello)
expect(greeter).to receive(:goodbye)
# The following will pass:
greeter.goodbye
greeter.hello
expect(greeter).to receive(:hello).ordered
expect(greeter).to receive(:goodbye).ordered
# The following will fail:
greeter.goodbye
greeter.hello
Code language: Ruby (ruby)
Рекомендуется использовать verify_partial_doubles
вместо partial_doubles
. По умолчанию отключено
только для обратной совместимости.
RSpec.configure do |config|
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
end
Code language: Ruby (ruby)
Тестирование API
В API тестах часто используется High-Fidelity Fakes. Как правило, это гемы которые имитируют поведение настоящего API. Часто такие гемы используются в купе с гемом VCR. После первого запроса этот гем записывает ответ, при повторном API запросе гем предоставляет ответ записанный ранее. Это позволят ускорить один тест примерно в 50 раз