Comments 17
Все ок, только по мне так примеры не очень. Спасибо за поднятую хорошую тему!
Let схож с subject’ом, но используется для объявления методов.
Может быть вы в курсе. Чем именованные subject отличается от let? Тем, что однострочниках распознается?
4. Собственные матчеры
Пример с матчером be_cool очень плох на мой взгляд. Вместо читаемого спека тот код, что там есть превратился в какую-то нечитаемую неинтуитивную магию, особенно, если описание матчера вынести в другое место. Мне кажется, в этом случае, лучше оставить только has_error_on, поскольку только этот пример действительно проповедует добро.
Это перевод документации что ли?
Не идеально, но очень неплохо :)
Из замечаний:
1. Про «примеры не очень» сказали, имхо имело смысл начать с let, subject и one-liner'ов, и везде ниже их использовать
2. Про shared_context не слышал, звучит интересно, но из описания тут не очень понял, как конкретно работает фича, пришлось лезть в доки, больше внимания деталям :)
3. Макросы можно было, наверное, не описывать — велика вероятность, что они скоро попадут в deprecated, а shared_example действительно целиком замещают их функционал.
4. В форматировании однострочников ошибка, там лишний отступ для It'ов и лишний end в конце :)
От себя, про важные мелочи:
Про describe: В сэмплах выше использовался describe со стрингой, но он умеет больше. В качестве параметра может принять класс klass, тогда он сам заинит subject как klass.new и будет поддерживать нужный неймспейс внутри тестов. Причем его можно дернуть внутри контекста существующих модулей, например. Очень удобно:
Про context: контекст — примерно то же самое, что и describe, кроме того что не изменяет subject. Хорошая практика их использовани — когда у нас контекст целиком состоит из атомарных спек (те самые one-liner'ы), а конкретное описание теста складывается из имен вложенных контекстов. См. примеры ниже про let и subject
Про let и subject: собственно отличаются они лишь тем, что subject указывает на тестируемый объект и в однострочниках expectation'ы автоматически связываются именно с ним, если вызваны не на каком-то конкретном объекте. Есть важные замечание про их природу.
let — это лямбда, которая лениво вычисляется при первом обращении внутри теста, и в let сохраняется результат вычисления. Так что если у вас есть какое-то тяжелое вычисление, которое может просадить тесты — не бойтесь запихать его в let, оно выполнится только если необходимо в тесте.
let'ы можно вызывать внутри друг друг, переопределять внутри контекста и т.д.
Можно хитрить и заставить let возвращать лямбду, тогда можно сократить на синтаксисе еще немного :) Не всегда это хорошо, но иногда уместно.
Только стоит помнить, что let вычисляется ровно один раз. Потому если вы решите его использовать как шорт-кат для измерения какого-то значения, то вас ждет провал :)
Важно помнить, что let вычисляется лениво, и если его выполнение имеет какие-то сайд-эффекты, то ленивое вычисление может привести к наступанию на грабли. Пример не очень хорош, но пояснит идею.
Эта спека провалится. Связано, очевидно, с тем, что список всех айдишников составляется до того, как будет вызвал let(:user), и на базке будет создан юзер. Соответственно делать let'ы надо без сайд-эффектов, что б на такие грабли не наступать. Ну или помнить о них. Чувствительные сайд-эффекты лучше явно указать в before.
В общем, let'ы очень удобны и довольно мощны, рекомендую их щедро использовать.
Пояснение про shared_context: Помимо того, что это переносной контекст, обращу внимание на одну фичу, которая из текста не очень ясна. Существует два метода его использования в спеках.
Я не 100% уверен в точности интерпретации, нет возможности потестить, но из док получается так
Из кратких замечаний наверное все %)
Еще хотел бы прочитать чужое мнение про тестирование методов с помощью should_receive — там вечные проблемы с лаконичностью и все не очень просто. Может и сам напишу вариант, попозже :)
Из замечаний:
1. Про «примеры не очень» сказали, имхо имело смысл начать с let, subject и one-liner'ов, и везде ниже их использовать
2. Про shared_context не слышал, звучит интересно, но из описания тут не очень понял, как конкретно работает фича, пришлось лезть в доки, больше внимания деталям :)
3. Макросы можно было, наверное, не описывать — велика вероятность, что они скоро попадут в deprecated, а shared_example действительно целиком замещают их функционал.
4. В форматировании однострочников ошибка, там лишний отступ для It'ов и лишний end в конце :)
От себя, про важные мелочи:
Про describe: В сэмплах выше использовался describe со стрингой, но он умеет больше. В качестве параметра может принять класс klass, тогда он сам заинит subject как klass.new и будет поддерживать нужный неймспейс внутри тестов. Причем его можно дернуть внутри контекста существующих модулей, например. Очень удобно:
module Project
module Models
describe User do # subject стал User.new
its(:value) { should be_nil }
specify { expect { subject.test }.to raise_error Errors::UserError }
# Не Project::Models::Errors::UserError
end
end
end
Про context: контекст — примерно то же самое, что и describe, кроме того что не изменяет subject. Хорошая практика их использовани — когда у нас контекст целиком состоит из атомарных спек (те самые one-liner'ы), а конкретное описание теста складывается из имен вложенных контекстов. См. примеры ниже про let и subject
Про let и subject: собственно отличаются они лишь тем, что subject указывает на тестируемый объект и в однострочниках expectation'ы автоматически связываются именно с ним, если вызваны не на каком-то конкретном объекте. Есть важные замечание про их природу.
let — это лямбда, которая лениво вычисляется при первом обращении внутри теста, и в let сохраняется результат вычисления. Так что если у вас есть какое-то тяжелое вычисление, которое может просадить тесты — не бойтесь запихать его в let, оно выполнится только если необходимо в тесте.
let'ы можно вызывать внутри друг друг, переопределять внутри контекста и т.д.
context "#request" do
let(:type) { 'correct_type' }
let(:params) { {type: type} }
subject { Interface.new.request(params) }
it { should be_ok }
context "with incorrect type" do
let(:type) { 'madness' }
it { should_not be_ok }
end
end
Можно хитрить и заставить let возвращать лямбду, тогда можно сократить на синтаксисе еще немного :) Не всегда это хорошо, но иногда уместно.
context "#request" do
let(:interface_call) { ->{ subject.some_call(type) } }
context "with incorrect type" do
let(:type) { 'Invalid type' }
specify { interface_call.should raise_error Errors::InvalidTypeError }
end
end
Только стоит помнить, что let вычисляется ровно один раз. Потому если вы решите его использовать как шорт-кат для измерения какого-то значения, то вас ждет провал :)
context "#add" do
let(:value) { subject.some_hardly_accessible_value.to_i }
specify { subject.add(20).should chage { value }.by(20) }
# Провалится, value вычислится один раз и больше не изменится
end
Важно помнить, что let вычисляется лениво, и если его выполнение имеет какие-то сайд-эффекты, то ленивое вычисление может привести к наступанию на грабли. Пример не очень хорош, но пояснит идею.
context "#collect_ids" do #Например, метод собирает из базы айдишники всех пользователей.
let(:user) { Fabricate(:user) } # Фабрикатором cоздается запись про юзера в базке
specify { User.all.collect_ids.should include user.id }
end
Эта спека провалится. Связано, очевидно, с тем, что список всех айдишников составляется до того, как будет вызвал let(:user), и на базке будет создан юзер. Соответственно делать let'ы надо без сайд-эффектов, что б на такие грабли не наступать. Ну или помнить о них. Чувствительные сайд-эффекты лучше явно указать в before.
В общем, let'ы очень удобны и довольно мощны, рекомендую их щедро использовать.
Пояснение про shared_context: Помимо того, что это переносной контекст, обращу внимание на одну фичу, которая из текста не очень ясна. Существует два метода его использования в спеках.
shared_context "shared_context", state: :a do # в метаданных указан state: :a - это важно
# some context
end
describe "#direct include" do
include_context "shared stuff" # прямо инклюдим shared_context в наш текущий контекст
context "subcontext_with_a_state", state: :a do
#some specs
end
context "subcontext_with_b_state", state: :b do
#other specs
end
#оба контекста будут включать в себя shared_context
end
describe "#metadata include" do
context "subcontext_with_a_state", state: :a do
#some specs
end
context "subcontext_with_b_state", state: :b do
#other specs
end
#только первый контекст включит в себя shared_context, так как совпадет по метадате
end
Я не 100% уверен в точности интерпретации, нет возможности потестить, но из док получается так
Из кратких замечаний наверное все %)
Еще хотел бы прочитать чужое мнение про тестирование методов с помощью should_receive — там вечные проблемы с лаконичностью и все не очень просто. Может и сам напишу вариант, попозже :)
Важно помнить, что let вычисляется лениво, и если его выполнение имеет какие-то сайд-эффекты, то ленивое вычисление может привести к наступанию на грабли. Пример не очень хорош, но пояснит идею.
Если леность мешает, то ее можно отменить, использовав let с восклицательным знаком:
let!(:user) { Fabricate(:user) }
Я так понимаю, что в инклуде у вас опечатка и должно быть «include_context „shared_context“».
Вот только непонятно, как во втором дескрайбе появился контекст, он же явно не инклудится.
Вот только непонятно, как во втором дескрайбе появился контекст, он же явно не инклудится.
Да, действительно опечатка. А вот с неявным инклюдом shared_context — это очень крутая фича, о которой, собственно, и речь, а автор поста не обратил на неё особого внимания.
Смотрите, вот рабочий тест, специально написал и проверил, и вам заодно покажу :)
У shared_context явно прописана метадата {state => :a}. Соответственно, в те контексты, которые имеют такую метадату (а соответственно и во все контексты-потомки) подключится этот shared_context. Тут есть сложные вопросы, как работает матч (полное совпадение, частичное? в каком порядке инклюдятся эти контексты, и т.д., но суть ясна.
Как это использовать — довольно очевидно. Например, у нас есть код, работающий с логикой, и часть этого кода активно использует EventMachine. Для таких тестов требуется отдельная инициализация этой самой эвент-машины, что вы и вынесли в shared_context.
К слову, есть такой отличный ключ в конфиге rspec'а:
Можете сами догадаться, что он делает :D
Теперь тесты, подключающие эвентмашину, стали выглядеть так:
Соответствующий код перекочевал в shared_context, обосновался в своем файлике с прозрачным именем, и мы выкосили его из конфига в спек-хелпере (раньше ручками проверяли метадату и хачили окружение теста как надо)
Смотрите, вот рабочий тест, специально написал и проверил, и вам заодно покажу :)
require 'rspec'
shared_context "shared_context", state: :a do
let(:val) { 2 }
end
describe 'shared context' do
let(:val) { 1 }
context 'included by metadata', state: :a do
specify { val.should == 2 }
end
context 'not included with distinct metadata', state: :b do
specify { val.should == 1 }
end
end
У shared_context явно прописана метадата {state => :a}. Соответственно, в те контексты, которые имеют такую метадату (а соответственно и во все контексты-потомки) подключится этот shared_context. Тут есть сложные вопросы, как работает матч (полное совпадение, частичное? в каком порядке инклюдятся эти контексты, и т.д., но суть ясна.
Как это использовать — довольно очевидно. Например, у нас есть код, работающий с логикой, и часть этого кода активно использует EventMachine. Для таких тестов требуется отдельная инициализация этой самой эвент-машины, что вы и вынесли в shared_context.
К слову, есть такой отличный ключ в конфиге rspec'а:
RSpec.configure do |config|
config.treat_symbols_as_metadata_keys_with_true_values = true
end
Можете сами догадаться, что он делает :D
Теперь тесты, подключающие эвентмашину, стали выглядеть так:
context 'some logic state', :eventmachine do
..
end
Соответствующий код перекочевал в shared_context, обосновался в своем файлике с прозрачным именем, и мы выкосили его из конфига в спек-хелпере (раньше ручками проверяли метадату и хачили окружение теста как надо)
Спасибо, выходит очень лаконично. Жаль, только, что в последнем случае нельзя контекст вызывать как:
context :eventmachine do
…
end
Кстати, а у Вас есть опыт тестирования эвентмашины, не хотите поделиться этим, может быть даже в статье?
context :eventmachine do
…
end
Кстати, а у Вас есть опыт тестирования эвентмашины, не хотите поделиться этим, может быть даже в статье?
Ну мы не тестировали саму EM, мы используем очень ограниченный её функционал — она обеспечивает нам на сервере глобальную очередь c таймерами, собственно всё что нам надо — запустить эвентмашину и поднять эту самую очередь. А потому всё те же stub и should_receive и should change, никакой особой специфики. Как протестировать написанный на ней вебсервер я до сих пор толком не знаю :)
Агитирую вас не использовать пустые контексты, пусть всегда будут именованые — ведь эта группировка не с потолка же берется, должна быть за ней какая-то логика.
Агитирую вас не использовать пустые контексты, пусть всегда будут именованые — ведь эта группировка не с потолка же берется, должна быть за ней какая-то логика.
Я бы не стал писать веб сервер на EM, не для того она. Когда я писал на ней игру, то я ограничился тестированием только игровой логики, внутри EM без поднятия самой машины, используя should_receive на машиновских send'ах.
>> Агитирую вас не использовать пустые контексты, пусть всегда будут именованые — ведь эта группировка не с потолка же берется, должна быть за ней какая-то логика.
По сути это не пустой контекст, там вресто имени используется шаред контекст, чего rspec не позволяет.
Можно было бы написать context :with_eventmachine do, вместо context 'with eventmachine' :with_eventmachine do
>> Агитирую вас не использовать пустые контексты, пусть всегда будут именованые — ведь эта группировка не с потолка же берется, должна быть за ней какая-то логика.
По сути это не пустой контекст, там вресто имени используется шаред контекст, чего rspec не позволяет.
Можно было бы написать context :with_eventmachine do, вместо context 'with eventmachine' :with_eventmachine do
Ну в любом случае, что ж вы как не родной-то! Это ж руби, а спеки — ваш локальный код, всегда можно подкрутить чего-нибудь по желанию ;)
require 'rspec'
class RSpec::Core::ExampleGroup
class << self
alias_method :old_context, :context
end
def self.context *args, &block
args[0].is_a?(Symbol) ? old_context(args[0].to_s, *args, &block) : old_context(*args, &block)
end
end
RSpec.configure do |config|
config.treat_symbols_as_metadata_keys_with_true_values = true
end
shared_context "shared_context", :key do
let(:val) { 2 }
end
describe 'shared context' do
let(:val) { 1 }
context :key do
specify { val.should == 2 }
end
context :other_key do
specify { val.should == 1 }
end
end
Тут очень тонкая грань. Другой человек потом посмотрит и подумает, что это стандартная функция и в других проектах будет так писать. А контекст возьми, да и не подключись, но ошибки не выдаст. Я вон минут 20 голову ломал, почему контекст не подключается если параметр только один =)
В целом согласен, но мне кажется, что если это решение будет использоваться в коде регулярно, а не 1-2 раза, то почему бы и нет — и человек рано или поздно увидит и этот код с переопределением контекста, или спросит «почему везде так, а у нас не так», или заметит потом, что написанные им на другом проекте работают не так, как надо (хотя потратит час на дебаг — но всего лишь один раз :) )
У меня был схожий случай, когда сам дописал нетривиальный метод в RSpec, для вызова оригинального метода при использовании should_receive в сложных тестах, но тут вышла версия 2.12, где появился метод and_call_original :) Там, конечно, понятнее было, но сам факт, что мы расширяли стандартное поведение — на лицо, и нам это было только на руку.
У меня был схожий случай, когда сам дописал нетривиальный метод в RSpec, для вызова оригинального метода при использовании should_receive в сложных тестах, но тут вышла версия 2.12, где появился метод and_call_original :) Там, конечно, понятнее было, но сам факт, что мы расширяли стандартное поведение — на лицо, и нам это было только на руку.
Кстати, пока писал этот сэмпл, полез в код RSpec'а, узнал чем отличаются describe и context.
Правильный ответ: ничем :)
Правильный ответ: ничем :)
# rspec-core / lib / rspec / core / example_group.rb
class << self
alias_method :context, :describe
end
В статье слабо упоминается про Factory Girl, начинающим советовал бы заострить на ней побольше внимания.
В догонку betterspecs.org :)
Sign up to leave a comment.
Применение принципа DRY в RSpec