Рубрики
Rspec Программирование Тестирование

Rspec заметки

:aggregate_failures

Обычно Rspec останавливается на первой ошибке и не показывает остальные, если мы хотим показать остальные ошибки, тогда в примере (‘it’) нужно добавить тег ‘:aggregate_failures’

it 'successfully saves the expense in the DB', :aggregate_failures doCode 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
endCode language: Ruby (ruby)

Результат:

Before :suite
Before :all
spec1
After :all
Before :all
spec2
After :all
After :suiteCode 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
endCode 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"
endCode language: Ruby (ruby)

Nesting (вложенность)

:aggregate_failures — показывает все тесты с ошибками. Дополнительные настройки можно установить с помощью блока. На пример в блоке ниже можно добавить if выражение, которое поможет пропускать некоторые примеры

RSpec.configure do |config| 
  config.define_derived_metadata do |meta|
    # Безоговорочно устанавливает флаг;
    # не позволяет примерам отказаться
    meta[:aggregate_failures] = true
  end
endCode language: Ruby (ruby)

Запуск определенных примеров по тегу

Бывает полезно когда мы работам над определенным функционалом. Что бы не запускать все тесты, запустим только тесты у которых есть определенный тег
( filter_run_including синоним filter_run )

RSpec.configure do |config|
  config.filter_run_excluding :jruby_only unless RUBY_PLATFORM == 'python'
endCode 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) #trueCode 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) # trueCode 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_emptyCode language: PHP (php)

Пример когда будут полезны предикаты

expect(user.admin?).to eq(true)
# Тест не пройдетCode language: CSS (css)

Нужно использовать это:

expect(user).to be_admin  или
expect(user).to be_an_adminCode 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_emptyCode 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_stdoutCode 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 
#=> nilCode language: Ruby (ruby)

Если мы хотим получить a_message нужно делать так:

expect(some_existing_object).to receive(:a_message).and_call_originalCode 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.3Code 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_originalCode 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) 
endCode language: Ruby (ruby)

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

Настройка аргументов (константы) Setting-constraints

allow(PasswordHash).to receive(:hash_password) 
  .and_wrap_original do |original, cost_factor|    
 original.call(1endCode 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
endCode 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).timesCode language: CSS (css)

Минимум и максимум то же поддерживаются

expect(client).to receive(:current_price).at_least(3).times
expect(client).to receive(:current_price).at_most(10).timesCode 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.helloCode language: Ruby (ruby)

Рекомендуется использовать verify_partial_doubles
вместо partial_doubles. По умолчанию отключено
только для обратной совместимости.

RSpec.configure do |config|
  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
  end
endCode language: Ruby (ruby)

Тестирование API

В API тестах часто используется High-Fidelity Fakes. Как правило, это гемы которые имитируют поведение настоящего API. Часто такие гемы используются в купе с гемом VCR. После первого запроса этот гем записывает ответ, при повторном API запросе гем предоставляет ответ записанный ранее. Это позволят ускорить один тест примерно в 50 раз

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *