Yet another rspec cheatsheet
Краткая выжимка неплохого курса по тестированию в свободной интерпретации и личными дополнениями. Этот cheatsheet содержит базовый функционал по работе с основными методами библиотеки rspec.
Изначально это был мини курс, который назывался “Rspec простым языком”. Он выдавался джунам, которых нужно было минимально научить пользоваться рспеком. После прочтения также выдавалить тестовые задания на закрепление.
Тестовые задания я решил убрать и выложить эту простую и супер-краткую версию курса как cheatsheet по rspec :)
Базовый функционал
Базовый пример теста
Тестируемый файл:
class Card
attr_accessor :rank, :suit
def initialize(rank, suit)
@rank = rank
@suit = suit
end
end
файл с тестами:
RSpec.describe Card do
let(:card) { Card.new('Ace', 'Spades') }
it 'has a rank and that rank can change' do
expect(card.rank).to eq('Ace')
card.rank = 'Queen'
expect(card.rank).to eq('Queen')
end
it 'has a suit' do
expect(card.suit).to eq('Spades')
end
it 'has a custom error message' do
card.suit = 'Nonsense'
comparison = 'Spades'
expect(card.suit).to eq(comparison), "Я ждал #{comparison} но получил #{card.suit}!"
end
end
describe
Метод Rpsec, который говорит что внутри него будет тестироваться какой-то объект нашей ситемы, в данном случае класс Card.
it
Конкретный тест, проверяющий описанный далее сценарий. Этот блок называется example, в будущем я буду называть эти блоки тестами.
В примере выше первый блок it проверяет что у объекта card, который принадлежит классу Card, есть метод rank и его можно назначать и читать. В примере у нас есть три теста(example), каждый из которых проверяет отдельный функционал.
Каждый из блоков должен быть независимым от других, т.е. не должно быть ситуации, в которой успешность теста зависит от порядка его запуска.
let
Переменная в мире rspec. Особенность ее в том, что она пересоздается перед стартом каждого example(it блок). Поэтому если в каком-то блоке с ней произошли какие-то изменения, то это не будет влиять на другие блоки.
В примере выше создается переменная card и она используется в каждом тесте.
expect
Основной метод библиотеки. Используется для заключения что тест прошел успешно или неуспешно.
Более простой пример:
RSpec.describe Integer do
it 'should pass' do
expect(1 + 1).to eq(2)
end
end
Результат запуска этого теста в консоли:
.
Finished in 0.00335 seconds (files took 0.12793 seconds to load)
1 example, 0 failures
В этом ответе мы видим что прошел один тест, ошибок не было.
Если поменять на ложную проверку
RSpec.describe Integer do
it 'should not pass' do
expect(1 + 1).to eq(3)
end
end
Результат:
F
Failures:
1) Integer should pass
Failure/Error: expect(1 + 1).to eq(3)
expected: 3
got: 2
(compared using ==)
# ./test.rb:3:in `block (2 levels) in <top (required)>'
Finished in 0.0232 seconds (files took 0.10606 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./test.rb:2 # Integer should pass
В этом результате мы видим, что всего 1 example и 1 фейл. Также написано что по тесту мы ждали, что там будет значение 3, но (сюрприз!) 1+1 оказалось равно двум, поэтому наш тест упал. На этом методе базируются все проверки.
subject
Прединициализированный объект, который является инстансом описываемого объекта. Пример неявного(implicit) использования:
RSpec.describe Hash do
it 'should start off empty' do
expect(subject.length).to eq(0)
subject[:some_key] = "Some Value"
expect(subject.length).to eq(1)
end
it 'is isolated between examples' do
expect(subject.length).to eq(0)
end
end
В примере выше мы описываем класс Hash
, значит по всех тестах ниже мы можем обращаться к объекту subject
, который является результатом Hash.new
.
Если бы в верхнем блоке былоRSpec.describe MyClass
, то в subject
хранился бы результат MyClass.new
Пример явного(explicit) использования:
RSpec.describe Hash do
subject(:bob) do
{ a: 1, b: 2 }
end
it 'has two key-value pairs' do
expect(subject.length).to eq(2)
expect(bob.length).to eq(2)
end
describe 'nexted example' do
it 'has two key-value pairs' do
expect(subject.length).to eq(2)
expect(bob.length).to eq(2)
end
end
end
Как видно из примера выше, содержимое subject
можно указывать явно.
Hooks
Регулярно в тестах встречаются моменты, когда перед каждым тестом нужно выполнить какую-то функцию или набор функций. Либо же после каджого теста. Для таких моментов существуют хуки. Они позволяют сокращать дубликацию кода и не нарушать DRY. Основные хуки:
- before(:context)
Выполняется один раз перед всеми тестами в этом блоке describe - after(:context)
Выполняется один раз после всех тестов в этом блоке describe - before(:example)
Выполняется каждый раз перед каждым тестом в этом блоке describe - after(:example)
Выполняется каждый раз после каждого теста в этом блоке describe
Пример
RSpec.describe 'before and after hooks' do
before(:context) do
puts 'Before context'
end
after(:context) do
puts 'After context'
end
before(:example) do
puts 'Before example'
end
after(:example) do
puts 'After example'
end
it 'is just a random example' do
expect(5 * 4).to eq(20)
end
it 'is just another random example' do
expect(3 - 2).to eq(1)
end
end
Результат запуска:
Before context
Before example
After example
.Before example
After example
.After context
Finished in 0.00352 seconds (files took 0.18995 seconds to load)
2 examples, 0 failures
Context
Блок, позволяющий описать в каком контексте мы находимся. Упрощает чтение тестов, позволяет группировать тесты вместе по общему контексту Пример:
RSpec.describe '#even? method' do
context 'with even number' do
it 'should return true' do
expect(4.even?).to eq(true)
end
end
context 'with odd number' do
it 'should return false' do
expect(5.even?).to eq(false)
end
end
end
Функционально не влияет ни на что. Позволяет для человека более ясно описать контекст, в котором происходит тест
Matchers
Матчеры, которые используются для сравнения значений в expect
выражениях. Позволяют гибко проверять успешность прохождения теста.
not_to
В общем-то противоположность метода .to
Пример:
RSpec.describe 'not_to method' do
it 'checks for the inverse of a matcher' do
expect(5).not_to eq(10)
expect([1, 2, 3]).not_to equal([1, 2, 3])
expect(10).not_to be_odd
expect([1, 2, 3]).not_to be_empty
expect(nil).not_to be_truthy
expect('Philadelphia').not_to start_with('car')
expect('Philadelphia').not_to end_with('city')
expect(5).not_to respond_to(:length)
expect([:a, :b, :c]).not_to include(:d)
expect { 11 / 3 }.not_to raise_error
end
end
eq vs eql vs equal
Так называемые equality matchers. Есть целых три варианта для сравнения объектов, но важно знать их разницу:
- eq
Под капотом сравнивает через знак ==. Проверяет совпадают ли значения объектов. Если объекты разные(int и float например), то он их скастит к одному общему типу - eql
Проверяет равенство значений объектов, но не пытается их скастить к общему типу, если их типы отличаются. - equal
Проверяет равенство объектов, т.е. он проверяет являются ли проверяемые объекты одним и тем же объектом(одинаковый object_id)
Пример:
RSpec.describe 'equality matchers' do
let(:a) { 3.0 }
let(:b) { 3 }
describe 'eq matcher' do
it 'tests for value and ignores type' do
expect(a).to eq(3)
expect(b).to eq(3.0)
expect(a).to eq(b)
end
end
describe 'eql matcher' do
it 'tests for value, including same type' do
expect(a).not_to eql(3)
expect(b).not_to eql(3.0)
expect(a).not_to eql(b)
expect(a).to eql(3.0)
expect(b).to eql(3)
end
end
describe 'equal and be matcher' do
let(:c) { [1, 2, 3] }
let(:d) { [1, 2, 3] }
let(:e) { c }
it 'cares about object identity' do
expect(c).to eq(d)
expect(c).to eql(d)
expect(c).to equal(e)
expect(c).not_to equal(d)
expect(c).not_to equal([1, 2, 3])
expect(:my_symbol).to equal(:my_symbol)
expect('my string').not_to equal('my string')
end
end
end
Все тесты в этом примере пройдут успешно.
Обрати внимание на строку expect('my string').not_to equal('my string')
, когда мы создаем строку таким образом, это будут разные объекты. Поэтому матчер equal этого не простит.
Но в то же время строка expect(:my_symbol).to equal(:my_symbol)
спокойно пройдет. Потому что Symbol такой класс, который по своим характеристикам больше похож на Integer, чем на String, он иммутабельный и каждый symbol уникален. Каждый раз, когда вы видите в коде :abc
, это один и тот же объект.
Более подробно прочитать про Symbol’ы в руби можно в этой статье.
Краткая разница String и Symbol:
irb(main):001:0> puts "string".object_id
47217136322240
=> nil
irb(main):002:0> puts "string".object_id
47217134571480
=> nil
irb(main):003:0> puts :symbol.object_id
885148
=> nil
irb(main):004:0> puts :symbol.object_id
885148
=> nil
Семейство be методов
Пример:
RSpec.describe 'be matchers' do
it 'can test for truthiness' do
expect(true).to be_truthy
expect('Hello').to be_truthy
expect(5).to be_truthy
expect(0).to be_truthy
expect(-1).to be_truthy
expect(3.14).to be_truthy
expect([]).to be_truthy
expect([1, 2]).to be_truthy
expect({}).to be_truthy
expect(:symbol).to be_truthy
end
it 'can test for falsiness' do
expect(false).to be_falsy
expect(nil).to be_falsy
end
it 'can test for nil' do
expect(nil).to be_nil
my_hash = { a: 5 }
expect(my_hash[:b]).to be_nil
end
it 'can be tested with predicate matchers' do
expect(16 / 2).to be_even
expect(15).to be_odd
expect(0).to be_zero
expect([]).to be_empty
end
end
be_truthy
- падает если объект является nil или false, во всех остальных случаях проходит успешно.
be_falsy
- проходит если объект nil или false
be_nil
- проходит, если объект является nil
be
- без параметров то же самое что и be_truthy
be_even
- объект должен быть четным
be_odd
- объект должен быть нечетным
Семейство этих методов еще раз показывает сахарность руби :)
Методы сравнения(comparison)
Иногда нужно сравнить объект с другим объектом, чтобы проверить успешность теста
Пример:
RSpec.describe 'comparison matchers' do
it 'allows for comparison with built-in Ruby operators' do
expect(10).to be > 5
expect(8).to be < 15
expect(1).to be >= -1
expect(1).to be >= 1
expect(22).to be <= 100
expect(22).to be <= 22
end
describe 100 do
it { is_expected.to be > 90 }
it { is_expected.to be >= 100 }
it { is_expected.to be < 500 }
it { is_expected.to be <= 100 }
it { is_expected.not_to be > 105 }
end
end
В блоке describe 100 do
появляется сокращенная(one-line) версия обычных блоков it
. В данном случае is_expected
то же самое, что и expect(100)
.
all matcher
Проверяет, что все объекты в тестируемом объекте соотвествуют заданному условию
Пример:
RSpec.describe 'all matcher' do
it 'allows for aggregate checks' do
expect([5, 7, 9, 13]).to all(be_odd)
expect([4, 6, 8, 10]).to all(be_even)
expect([[], [], []]).to all(be_empty)
expect([0, 0]).to all(be_zero)
expect([5, 7, 9]).to all(be < 10)
end
describe [5, 7, 9] do
it { is_expected.to all(be_odd) }
it { is_expected.to all(be < 10) }
end
end
Более красивая замена циклу с expect’ами.
change matcher
Матчер, который проверяет состояние объекта
RSpec.describe 'change matcher' do
subject { [1, 2, 3, 4] }
it 'checks that a method changes object state' do
expect { subject.push(4) }.to change { subject.length }.by(1)
end
it 'accepts negative arguments' do
expect { subject.pop }.to change { subject.length }.from(4).to(3)
expect { subject.pop }.to change { subject.length }.by(-1)
end
end
В первом тесте мы пихаем в subject новый элемент и проверяем, что после этого длинна объекта поменялась. На этом примере может показаться, что это бесполезный тест, но этот метод может понадобиться для проверки более сложных итерабельный структур.
start_with, end_with matchers
Проверяют, что объект начинается или завершается чем-то
RSpec.describe 'start_with and end_with matchers' do
describe 'caterpillar' do
it 'should check for substring at the beginning or end' do
expect(subject).to start_with('cat')
expect(subject).to end_with('pillar')
end
it { is_expected.to start_with('cat') }
it { is_expected.to end_with('pillar') }
end
describe [:a, :b, :c, :d] do
it 'should check for elements at beginning or end of the array' do
expect(subject).to start_with(:a)
expect(subject).to start_with(:a, :b)
expect(subject).to start_with(:a, :b, :c)
expect(subject).to end_with(:d)
expect(subject).to end_with(:c, :d)
end
it { is_expected.to start_with(:a, :b) }
end
end
have_attributes
Матчер, который проверяет что переданный объект имеет определенные аттрибуты
class ProfessionalWrestler
attr_reader :name, :finishing_move
def initialize(name, finishing_move)
@name = name
@finishing_move = finishing_move
end
end
RSpec.describe 'have_attributes matcher' do
describe ProfessionalWrestler.new('Stone Cold Steve Austin', 'Stunner') do
it 'checks for object attribute and proper values' do
expect(subject).to have_attributes(name: 'Stone Cold Steve Austin')
expect(subject).to have_attributes(name: 'Stone Cold Steve Austin', finishing_move: 'Stunner')
end
it { is_expected.to have_attributes(name: 'Stone Cold Steve Austin') }
it { is_expected.to have_attributes(name: 'Stone Cold Steve Austin', finishing_move: 'Stunner') }
end
end
Для простоты я указал класс и тест в одном файле.
include
Проверяет, что объект содержит другой объект
RSpec.describe 'include matcher' do
describe 'hot chocolate' do
it 'checks for substring inclusion' do
expect(subject).to include('hot')
expect(subject).to include('choc')
expect(subject).to include('late')
end
it { is_expected.to include('choc') }
end
describe [10, 20, 30] do
it 'checks for inclusion in the array, regardless of order' do
expect(subject).to include(10)
expect(subject).to include(10, 20)
expect(subject).to include(30, 20)
end
it { is_expected.to include(20, 30, 10) }
end
describe ({ a: 2, b: 4 }) do
it 'can check for key existence' do
expect(subject).to include(:a)
expect(subject).to include(:a, :b)
expect(subject).to include(:b, :a)
end
it 'can check for key-value pair' do
expect(subject).to include(a: 2)
end
it { is_expected.to include(:b) }
it { is_expected.to include(b: 4) }
end
end
raise_error
Более интересный матчер, проверяет что блок слева рейсает определенную ошибку
def some_method
x
end
class CustomError < StandardError; end
RSpec.describe 'raise_error matcher' do
it 'can check for a specific error being raised' do
expect { some_method }.to raise_error(NameError) # undefined local variable or method `x`
expect { 10 / 0 }.to raise_error(ZeroDivisionError)
end
it 'can check for a user-created error' do
expect { raise CustomError }.to raise_error(CustomError)
end
end
Полезно при проверке каких-то критичных значений, которые должны рейсать ошибки. При этом важно, что можно проверить какую именно ошибку рейсает блок из expect
respond_to
Проверяет имеет ли переданный объект какой-то метод
class HotChocolate
def drink
'Delicious'
end
def discard
'PLOP!'
end
def purchase(number)
"Awesome, I just purchased #{number} more hot chocolate beverages!"
end
end
RSpec.describe HotChocolate do
it 'confirms that an object can respond to a method' do
expect(subject).to respond_to(:drink)
expect(subject).to respond_to(:drink, :discard)
expect(subject).to respond_to(:drink, :discard, :purchase)
end
it 'confirms an object can respond to a method with arguments' do
expect(subject).to respond_to(:purchase)
expect(subject).to respond_to(:purchase).with(1).arguments
end
it { is_expected.to respond(:purchase, :discard) }
it { is_expected.to respond(:purchase).with(1).arguments }
end
expect(subject).to respond_to(:drink)
- тест пройдет, если subject.drink
определен.
Несколько expect’ов (Compound expectations)
Если нужно проверять сразу несколько условий после expect
а
RSpec.describe 25 do
it 'can test for multiple matchers' do
expect(subject).to be_odd.and be > 20
end
end
RSpec.describe 'caterpillar' do
it 'supports multiple matchers' do
expect(subject).to start_with('cat').and end_with('pillar')
end
it { is_expected.to start_with('cat').and end_with('pillar') }
end
RSpec.describe [:usa, :canada, :mexico] do
it 'can check for several possibilities' do
expect(subject.sample).to eq(:usa).or eq(:canada).or eq(:mexico)
end
end
Логика такая же, как и везде, с той лишь разницей, что тут это задается через .and
и .to
Mocks
Моки это всеобразные инструменты, позволяющие имитировать взаимодействие с чем-либо.
Например в тесте проверяется апи с каким-то внешним сервисом, но нельзя каждый раз при запуске тестов реально туда обращаться. Для этого есть моки, которые, например, позволют имитировать http запросы куда-то. Т.е. в коде они делаются, но в реальности никуда запрос не летит и возвращает заранее заготовленный респонс. То же самое можно делать с функциями, классами и чем угодно.
double(двойник)
double(дабл) позволяет имитировать любой объект, задавая его поведение динамически
RSpec.describe 'a random double' do
it 'only allows defined methods to be invoked' do
stuntman = double("Mr. Danger")
allow(stuntman).to receive_messages(fall_off_ladder: 'Ouch', light_on_fire: true)
expect(stuntman.fall_off_ladder).to eq('Ouch')
expect(stuntman.light_on_fire).to eq(true)
end
end
На 3 строке мы объявляем пустой дабл, строка "Mr. Danger"
просто для идентификации и описания что это за дабл. На 4й строке методом allow
мы задаем, что в дабле stuntman
есть 2 метода: fall_off_ladder
, который возвращает строку Ouch
и метод light_on_fire
, который возвращает true
На самом деле правильно в руби говорить, что объект получает сообщение(поэтому метод и называется receive_messages
), но для простоты я говорю, что этот дабл имеет методы fall_off_ladder
и light_on_fire
.
Соответственно в expect’ах на 5 и 6 строках и проверяется, что дабл реализует два вышеописанных метода.
Использование двойников очень полезно, когда нам нужно изолировать тест, не делая его слишком зависимым от других систем. Например нам нужно изолированно протестировать класс А, который взаимодействует с классами В и С. Разумной идеей может быть замена взаимодействий с классами В и С через двойников, потому что когда мы проверяем исключительно класс А, мы не должны быть зависимыми от поломок в классах В и С(речь про изолированные тесты, а не про тестирование взаимодействия между этими классами).
receive counts
Можно проверять сколько раз вызывался тот или иной метод с разной вариацией проверок
RSpec.describe 'a random double' do
it 'expects call stuntman.fall_off_ladder exactly 3 times' do
stuntman = double("Mr. Danger")
allow(stuntman).to receive_messages(fall_off_ladder: 'Ouch', light_on_fire: true)
expect(stuntman).to receive(:fall_off_ladder).exactly(3).times
stuntman.fall_off_ladder
stuntman.fall_off_ladder
stuntman.fall_off_ladder
end
it 'expects call stuntman.fall_off_ladder at least 2 times' do
stuntman = double("Mr. Danger")
allow(stuntman).to receive_messages(fall_off_ladder: 'Ouch', light_on_fire: true)
expect(stuntman).to receive(:fall_off_ladder).at_least(2).times
stuntman.fall_off_ladder
stuntman.fall_off_ladder
stuntman.fall_off_ladder
stuntman.fall_off_ladder
end
end
В первом тесте мы проверяем, что метод fall_off_ladder
вызывался именно 3 раза, во втором тесте проверяем что этот метод вызывался минимум 2 раза.
Важно заметить, что receive
проверяет что метод вызывался в рамках текущего теста, т.е. не к моменту где стоит expect(stuntman).to receive(:fall_off_ladder)
, а до конца текущего теста. Как проверяет это прямо на строке с expect будет описано далее.
allow
В предыдущем примере было видно, что метод allow
позволяет создавать заглушки на даблах, но он может также и переопределять функционал на любых объектах
RSpec.describe 'allow method review' do
it 'can customize return value for methods on doubles' do
calculator = double
allow(calculator).to receive(:add).and_return(15)
expect(calculator.add).to eq(15)
expect(calculator.add(3)).to eq(15)
expect(calculator.add(-2, -3 -5)).to eq(15)
expect(calculator.add('hello')).to eq(15)
end
it 'can stub one or more methods on a real object' do
arr = [1, 2, 3]
allow(arr).to receive(:sum).and_return(10)
expect(arr.sum).to eq(10)
arr.push(4)
expect(arr).to eq([1, 2, 3, 4])
end
it 'can return multiple return values in sequence' do
mock_array = double
allow(mock_array).to receive(:pop).and_return(:a, :b, :c)
expect(mock_array.pop).to eq(:a)
expect(mock_array.pop).to eq(:b)
expect(mock_array.pop).to eq(:c)
expect(mock_array.pop).to eq(:c)
expect(mock_array.pop).to eq(:c)
expect(mock_array.pop).to eq(:c)
end
end
В первом тесте мы задаем, что двойник calculator
имеет метод add
и в результате всегда возвращает 15 независимо от входных параметров.
Во втором тесте пример того, как allow
может переопределять методы любых объектов. В данном случае мы переопределили метод sum у класса Array в рамках этого теста и он всегда возвращает 10 независимо от своего содержания.
В третье тесте чуть менее интуитивный понятный, но полезный функционал. Когда мы перечисляем параметры метода and_return
, мы указываем порядок их возвращения при вызовах. Если мы указали параметры :a, :b, :c
, то при первом вызове метода mock_array.pop
мы получим :a
, при втором :b
и при третьем :c
. Важный момент, что все последующие вызовы мы также будем получать последний указанный элемент, в нашем случае :c
matching arguments
При установке заглушек можно также и указывать необходимые параметры
RSpec.describe 'matching arguments' do
it 'can return different values depending on the argument' do
three_element_array = double # [1, 2, 3]
allow(three_element_array).to receive(:first).with(no_args).and_return(1)
allow(three_element_array).to receive(:first).with(1).and_return([1])
allow(three_element_array).to receive(:first).with(2).and_return([1, 2])
allow(three_element_array).to receive(:first).with(be >= 3).and_return([1, 2, 3])
expect(three_element_array.first).to eq(1)
expect(three_element_array.first(1)).to eq([1])
expect(three_element_array.first(2)).to eq([1, 2])
expect(three_element_array.first(3)).to eq([1, 2, 3])
expect(three_element_array.first(100)).to eq([1, 2, 3])
end
end
На 5 строке мы задаем, что если вызывается метод first
без параметров, он возвращает 1
. На 6 и 7 строке мы указываем какой результат должен вернуть этот метод при разных входящих параметрах. На 8й задается результат, если входной параметр был больше 3х.
instance double
Более строгая версия double, которая на вход берет класс. Разница ее в том, что у этого дабла можно ставить заглушки только на те методы, что определены в переданном классе. Грубо говоря это возможность пользоваться объектом класса, не создавая реально этот объект.
class Person
def a(seconds)
sleep(seconds)
"Hello"
end
end
RSpec.describe Person do
describe 'regular double' do
it 'can implement any method' do
person = double(a: "Hello", b: 20)
expect(person.a).to eq("Hello")
end
end
describe 'instance double' do
it 'can only implement methods that are defined on the class' do
person = instance_double(Person)
allow(person).to receive(:a).with(3).and_return("Hello")
expect(person.a(3)).to eq("Hello")
end
end
end
В первом тесте для примера видно, что двойнику можно задать реализацию любого метода.
Во втором тесте используется instance_double
, у которого можно переопределять только те методы, что указаны в классе Person
.
Этот функционал позволяет более строго использовать двойника, исключая возможность в тесте задать ему такой функционал, который в реальности встречаться не будет никогда.
class double
Похожая ситуация как и с instance double, только теперь мы строим заглушку класса
class Deck
def self.build
# Business logic to build a whole bunch of cards
end
end
class CardGame
attr_reader :cards
def start
@cards = Deck.build
end
end
RSpec.describe CardGame do
it 'can only implement class methods that are defined on a class' do
deck_klass = class_double(Deck, build: ['Ace', 'Queen']).as_stubbed_const
expect(deck_klass).to receive(:build)
subject.start
expect(subject.cards).to eq(['Ace', 'Queen'])
end
end
На 17й строке создается двойник класса Deck
, также задается что возвращает метод build
. Двойник класса также может подменять только методы, который указаны в оригинальном классе.
Другая важная и очень полезная особенность это .as_stubbed_const
. Если задать двойник без нее, то в переменной deck_klass
будет храниться двойник класса Deck
, но оригинальный класс Deck
также остался в памяти. В нашем случае внутри класса CardGame
идет обращение к классу Deck
и мы никак не можем на это повлиять. Но если у двойника вызвать метод .as_stubbed_const
, то он собой заменяет оригинальный класс и когда CardGame
внутри обращается к Deck
, то попадет на нашего двойника.
spy
Spy(шпион) это аналог double, который работает слегка иначе. В двойнике мы говорим ему какие методы он реализует и проверяем обращались ли к нему по его методам через receive
. Шпиону не нужно указывать какие методы он реализует, он успешно сможет получать все методы, которые к нему обращаются, возвращая при этом nil.
RSpec.describe 'spies' do
let(:animal) { spy('animal') }
it 'confirms that a message has been received' do
animal.eat_food
expect(animal).to have_received(:eat_food)
expect(animal).not_to have_received(:eat_human)
end
it 'resets between examples' do
expect(animal).not_to have_received(:eat_food)
end
it 'retains the same functionality of a regular double' do
animal.eat_food
animal.eat_food
animal.eat_food('Sushi')
expect(animal).to have_received(:eat_food).exactly(3).times
expect(animal).to have_received(:eat_food).at_least(2).times
expect(animal).to have_received(:eat_food).with('Sushi')
expect(animal).to have_received(:eat_food).once.with('Sushi')
end
end
Еще одной отличительной чертой является использование метода have_received
вместо receive
. have_received
проверяет был ли вызыван указанный метод на шпионе в данный момент, тогда как receive
будет проверять, был ли вызыван этот метод до конца текущего теста.