(Я использую JRuby, но думаю, что моя проблема относится и к MRI Ruby)
Мое определение RSpec имеет следующую общую структуру:
RSpec.describe 'XXX' do
before(:all) do
# preparation common for each example
end
after(:all) do
# clean up after each example
end
context 'C1' do
it 'Ex1' do
....
end
it 'Ex2' do
....
end
....
end
context 'C2' do
.... # more examples here
end
конец
Проблема здесь в том, что подготовительная работа (before(:all)
) включает в себя вычисление значения, скажем
some_value=MyMod.foo()
который мне понадобится в коде очистки (after(:all)
), т.е.
MyMod.bar(some_value)
Конечно, поскольку some_value
— локальная переменная, я не могу этого сделать. Теоретически я мог бы использовать глобальную переменную,
before(:all) do
$some_value=MyMod.foo()
end
after(:all) do
MyMod.bar($some_value)
end
но это не только некрасиво, это вызовет проблемы, если я однажды решу распараллелить свои тесты.
Можно ли этого достичь? Или мой общий дизайн ошибочен?
ОБНОВЛЕНИЕ: Как и просили в комментарии, я объясняю здесь, почему я хочу это сделать:
Наше приложение имеет набор глобальных свойств (технически говоря, они хранятся в глобальном одноэлементном объекте), и этим «свойствам» присваиваются значения по умолчанию согласно спецификации этого приложения, т.е. в обычном случае они никогда не изменяются. . Однако в целях экспериментирования можно переопределить эти свойства во время запуска приложения (через переменные среды).
Следовательно, подавляющее большинство тестов RSpec просто используют эти значения по умолчанию. Однако некоторые модульные тесты также должны проверять случай, когда эти свойства имеют значение, отличное от значения по умолчанию. В настоящее время я пишу примеры так, что в начале примера я сохраняю текущий тест свойства в переменной, а в конце примера возвращаю porperty к исходному значению. Если я хочу сделать это для набора примеров, область действия которых находится в одном и том же блоке describe
или context
, я подумал, что было бы лучше сделать это изменение и сброс переменных в блоке before
или after
, чтобы делается единообразно во всех примерах этой группы.
🤔 А знаете ли вы, что...
Ruby поддерживает метапрограммирование, что позволяет изменять код программы во время выполнения.
Как уже отметил Стефан в комментариях, для этого вы можете использовать переменные экземпляра.
В частности, каждая группа примеров RSpec (т. е. блок describe
или context
) определяет класс, и любые примеры (it
, example
, specify
и т. д.) и хуки (before
, after
и around
) в группе оцениваются в контексте. экземпляра этого класса.
Таким образом, как показано в документации, вы можете определить переменную экземпляра (которая будет принадлежать этому экземпляру класса примера группы) в блоке before
и получить к ней доступ из любого блока it
или after
в той же группе (или во вложенных группы, которые наследуют перехватчики от своих родительских групп), например. так:
describe 'something' do
before(:each) do
@some_value = MyMod.foo()
end
after(:each) do
MyMod.bar(@some_value)
end
# Add your examples here...
end
(Здесь вы также можете использовать before(:all)
и after(:all)
, в этом случае перехваты выполняются только один раз для всей группы примеров, а не до и после каждого примера в группе. В остальном они работают точно так же. Обратите внимание, что перехватчики before(:suite)
, однако не может использоваться для определения таких переменных экземпляра, поскольку они оцениваются по-другому. В общем, я бы рекомендовал просто не использовать их.)
Альтернативное решение — использовать let!
для определения запоминаемого вспомогательного метода, который (из-за !
) автоматически вычисляется перед любыми примерами и чье сохраненное значение все еще доступно позже:
describe 'something' do
# This is evaluated in an implicit before hook before each example:
let!(:some_value) { MyMod.foo() }
after(:each) do
MyMod.bar(some_value)
end
# Add your examples here...
end
Одним из преимуществ этого подхода (помимо, возможно, более чистого синтаксиса с меньшим количеством знаков @
) является то, что some_value
нельзя переназначать в примерах (что может привести к тонким ошибкам). Однако его содержимое, конечно, может по-прежнему включать изменяемые объекты, поэтому это не гарантирует полной гарантии того, что настройки, восстановленные в блоке after
, остаются такими же, какими они были до примеров.
Кроме того, хотя вы можете установить несколько переменных экземпляра в одном блоке before
, let!
позволяет определить только один вспомогательный метод. Конечно, вы всегда можете обойти это ограничение с помощью чего-то вроде этого:
describe 'something' do
let!(:saved_settings) do
some_value = MyMod.foo()
other_value = MyMod.foo2()
[some_value, other_value].freeze
end
after(:each) do
some_value, other_value = saved_settings
MyMod.bar(some_value)
MyMod.bar2(other_value)
end
end
Еще одно (незначительное) ограничение этого подхода заключается в том, что let!
оценивается перед каждым примером, поэтому вы не можете оценивать его только один раз для каждой группы примеров, например before(:all)
. Однако, как правило, в любом случае рекомендуется очищать среду после каждого примера, поэтому на практике это не такое уж большое ограничение, как может показаться.
Еще одно решение, уже предложенное Тоддом А. Джейкобсом в комментариях, — это, конечно же, использовать круглый крючок вместо этого:
describe 'something' do
around(:each) do |example|
some_value = MyMod.foo()
example.run
MyMod.bar(some_value)
end
# Add your examples here...
end
Пожалуй, это самый аккуратный и гибкий метод. Однако у него есть то же ограничение, что и у let!
: хук всегда будет выполняться один раз для каждого примера; в RSpec нет поддержки хуков round(:all) . По крайней мере, без дополнительных сторонних драгоценных камней.
(Отказ от ответственности: я не тестировал драгоценный камень, указанный выше, я просто нашел его с помощью быстрого поиска в Google. Однако на первый взгляд код выглядит надежным, даже если он не обновлялся в течение десяти лет.)
Пс. Одна из распространенных ситуаций, напоминающих ваш пример, — это когда вы хотите изменить значение константы Ruby перед тестами (например, чтобы переключить флаг функции, заменить параметр конфигурации или даже имитировать весь класс) и впоследствии восстановить ее исходное значение. Для этого RSpec Mocks предоставляет конкретное решение в виде метода stub_const:
describe 'something' do
before do
stub_const('MyMod::FOO', 42)
end
# Add your examples here...
end
Приведенный выше код гарантирует, что MyMod::FOO
будет иметь значение 42 в ваших примерах, а также автоматически восстанавливает исходное значение после каждого примера. Обратите внимание, что вам даже не нужно определять перехватчик after
для восстановления исходного значения константы, поскольку RSpec Mocks позаботится об этом автоматически.
Вы также можете вызвать stub_const
непосредственно из блока примера (it
/ example
/ specify
), что еще больше сокращает шаблонный код, если вам нужна константа, заглушенная только для одного примера (или если вам нужно присвоить ей разные значения в разных примерах).
Я хочу сказать, что ваш общий дизайн, вероятно, ошибочен.
before(:all)
— это функция RSpec, которую следует избегать, за исключением особых случаев.
Если вы столкнулись с одним из тех редких случаев, когда преимущества производительности от выполнения вашей тестовой настройки один раз для каждой спецификации перевешивают недостатки и риски общего состояния, вы можете использовать переменные экземпляра для хранения переменной для всей спецификации.
Как в этом примере веб-скребка Я бесстыдно украл у Майрона Марстона:
describe "Scraping page X" do
before(:all) do
@scrape_summary = MyScraper.perform_on("http://foo-bar.com/bazz")
end
it "extracts one kind of thing from the page" do
@scrape_summary.total_results.should eq(25)
end
it "extracts something else from the page" do
@scrape_summary.link_urls.should include("http://some-expected-url.com/")
end
# ...several more of these sorts of examples
end
Здесь, выполняя шаги HTTP-вызова и анализа, тесты выполняются значительно быстрее. Здесь также важно, чтобы каждый пример был на 100% свободен от побочных эффектов и не изменялся @scrape_summary
.
Во всех остальных случаях использования следует использовать пусть и пусть! для настройки тестовых зависимостей и настройки/разборки для каждого примера. Это позволяет избежать взаимозависимостей между вашими примерами и ужасным примером «колебания». Они также потокобезопасны.
RSpec.describe 'Thingy' do
# The dependencies of our example group which are memoized per example
let(:foo) { Foo.new }
let(:bar) { Bar.new }
# do this before every example
before do
some_proceedural_step(foo)
end
# tear down if needed
after do
undo_some_proceedural_step
end
it "frobnobs the whatchamacallit" do
# ...
end
context "when foo is awesome" do
# let can be refined per example group
let(:foo) { Foo.new(awesome: true) }
it "superfrobnobs the whatchamacallit" do
# ...
end
end
end