Когда использовать RSpec let()?
Я склонен использовать перед блоками для установки переменных экземпляра. Затем я использую эти переменные в своих примерах. Я недавно натолкнулся let()
, Согласно документам RSpec, он используется для
... чтобы определить запомненный вспомогательный метод. Значение будет кэшировано для нескольких вызовов в одном и том же примере, но не во всех.
Чем это отличается от использования переменных экземпляра в до блоков? А также, когда вы должны использовать let()
против before()
?
9 ответов
Я всегда предпочитаю let
к переменной экземпляра по нескольким причинам:
- Переменные экземпляра возникают при обращении к ним. Это означает, что если вы нажмете правописание переменной экземпляра, будет создана новая и инициализирована
nil
, что может привести к тонким ошибкам и ложным срабатываниям. посколькуlet
создает метод, вы получитеNameError
когда вы ошиблись, что я считаю предпочтительным. Это также облегчает рефакторинг спецификаций. before(:each)
hook будет запускаться перед каждым примером, даже если в этом примере не используются переменные экземпляра, определенные в hook. Обычно это не имеет большого значения, но если установка переменной экземпляра занимает много времени, то вы тратите впустую циклы. Для метода, определенногоlet
код инициализации выполняется только в том случае, если его вызывает этот пример.- Вы можете выполнить рефакторинг из локальной переменной в примере непосредственно в let без изменения синтаксиса ссылок в примере. Если вы реорганизуете переменную экземпляра, вы должны изменить способ ссылки на объект в примере (например, добавить
@
). - Это немного субъективно, но, как отметил Майк Льюис, я думаю, что это облегчает чтение спецификации. Мне нравится организация определения всех моих зависимых объектов с
let
и держу мойit
Блок хороший и короткий.
Разница между использованием переменных экземпляров и let()
в том, что let()
лениво оценен. Это означает, что let()
не оценивается, пока метод, который он определяет, не будет запущен в первый раз.
Разница между before
а также let
в том, что let()
дает вам хороший способ определения группы переменных в "каскадном" стиле. При этом спецификация выглядит немного лучше за счет упрощения кода.
Я полностью заменил все использования переменных экземпляра в моих тестах rspec на использование let(). Я написал пример для друга, который использовал его для обучения небольшому классу Rspec: http://ruby-lambda.blogspot.com/2011/02/agile-rspec-with-let.html
Как говорится в некоторых других ответах, let() вычисляется лениво, поэтому он будет загружать только те из них, которые требуют загрузки. Это сушит спецификации и делает его более читабельным. На самом деле я перенес код Rspec let() для использования в моих контроллерах, в стиле gem длятекапак. http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html
Наряду с отложенной оценкой, другим преимуществом является то, что в сочетании с ActiveSupport::Concern и спецификацией / поддержкой / поведением загрузки всего, что вы можете, вы можете создать свой собственный спецификационный мини-DSL, специфичный для вашего приложения. Я написал их для тестирования ресурсов Rack и RESTful.
Стратегия, которую я использую, - "Фабрика-все" (через Машинист + Подделка / Подделка). Однако его можно использовать в сочетании с блоками before(:each) для предварительной загрузки фабрик для всего набора групп примеров, что позволяет спецификациям работать быстрее: http://makandra.com/notes/770-taking-advantage-of-rspec-s-let-in-before-blocks
Важно помнить, что let оценивается лениво и не содержит в себе методов побочных эффектов, иначе вы не сможете легко перейти от let к before(: каждому). Вы можете использовать пусть! вместо того, чтобы позволить так, чтобы это оценивалось перед каждым сценарием.
Несогласный голос здесь: после 5 лет рспек мне не нравится let
очень сильно.
1. Ленивая оценка часто затрудняет настройку теста.
Становится трудным рассуждать о настройке, когда некоторые вещи, которые были объявлены в настройке, на самом деле не влияют на состояние, а другие влияют.
В конце концов, от разочарования кто-то просто меняет let
к let!
(то же самое без ленивой оценки), чтобы их спецификация работала. Если у них это срабатывает, рождается новая привычка: когда новая спецификация добавляется к старому набору, а она не работает, первое, что пытается сделать писатель, - это добавить челку к случайномуlet
звонки.
Довольно скоро все преимущества производительности исчезнут.
2. Особый синтаксис необычен для пользователей, не использующих rspec.
Я лучше научу свою команду Ruby, чем трюкам rspec. Переменные экземпляра или вызовы методов полезны везде в этом и других проектах,let
синтаксис будет полезен только в rspec.
3. "Преимущества" позволяют нам легко игнорировать хорошие изменения дизайна.
let()
хорош для дорогостоящих зависимостей, которые мы не хотим создавать снова и снова. Он также хорошо сочетается сsubject
, позволяя избавиться от повторных вызовов методов с несколькими аргументами
Дорогостоящие зависимости, повторяющиеся много раз, и методы с большими сигнатурами - оба момента, где мы могли бы улучшить код:
- возможно, я смогу ввести новую абстракцию, которая изолирует зависимость от остальной части моего кода (что будет означать, что она понадобится меньшему количеству тестов)
- возможно, тестируемый код делает слишком много
- возможно мне нужно ввести более умные объекты вместо длинного списка примитивов
- возможно у меня нарушение принципа "говори-не-спрашивай"
- возможно, дорогой код можно сделать быстрее (реже - остерегайтесь преждевременной оптимизации)
Во всех этих случаях я могу устранить симптомы сложных тестов с помощью успокаивающего бальзама rspec magic или попытаться устранить причину. Мне кажется, что последние несколько лет я потратил слишком много на первое, а теперь мне нужен лучший код.
Чтобы ответить на исходный вопрос: я бы предпочел не делать этого, но я все еще использую let
. Я в основном использую его, чтобы соответствовать стилю остальной команды (похоже, что большинство программистов Rails в мире теперь глубоко в своей магии rspec, так что это происходит очень часто). Иногда я использую его, когда добавляю тест к некоторому коду, над которым у меня нет контроля или у меня нет времени на рефакторинг до лучшей абстракции: то есть когда единственный вариант - болеутоляющее.
В общем, let()
это хороший синтаксис, и он экономит вам, набрав @name
символы повсюду. Но будьте бдительны! я нашел let()
также вводит незначительные ошибки (или, по крайней мере, царапины на голове), потому что переменная на самом деле не существует, пока вы не попытаетесь ее использовать... Скажите знак сказки: если добавление puts
после let()
чтобы убедиться, что переменная верна, можно передать спецификацию, но без puts
спецификация терпит неудачу - вы нашли эту тонкость.
Я также обнаружил, что let()
похоже, не кэшируется при любых обстоятельствах! Я написал это в своем блоге: http://technicaldebt.com/?p=1242
Может это только у меня?
Пусть является функциональным, так как по сути это Proc. Также его кешируется.
Одна ошибка, которую я нашел сразу с помощью let... В блоке Spec, который оценивает изменение.
let(:object) {FactoryGirl.create :object}
expect {
post :destroy, id: review.id
}.to change(Object, :count).by(-1)
Вам нужно будет обязательно позвонить let
за пределами вашего ожидаемого блока. то есть ты звонишь FactoryGirl.create
в вашем блоке let. Я обычно делаю это, проверяя объект сохраняется.
object.persisted?.should eq true
В противном случае, когда let
Блок вызывается в первый раз, когда изменение в базе данных действительно произойдет из-за ленивого создания экземпляра.
Обновить
Просто добавляю заметку. Будьте осторожны, играя в гольф с кодом или в этом случае с этим ответом.
В этом случае мне просто нужно вызвать метод, на который отвечает объект. Поэтому я призываю _.persisted?
_ Метод на объекте как его правдивый. Все, что я пытаюсь сделать, это создать экземпляр объекта. Вы могли бы назвать пустым? или ноль? тоже. Дело не в тесте, а в том, чтобы оживить объект, вызвав его.
Таким образом, вы не можете рефакторинг
object.persisted?.should eq true
быть
object.should be_persisted
как объект не был создан... его ленивый.:)
Обновление 2
рычаг пусть! синтаксис для мгновенного создания объекта, который должен полностью избежать этой проблемы. Заметьте, однако, что это побеждает многие цели лени без взрыва.
Также в некоторых случаях вы можете использовать синтаксис субъекта вместо let, поскольку это может дать вам дополнительные опции.
subject(:object) {FactoryGirl.create :object}
"до" по умолчанию подразумевает before(:each)
, Ссылка на книгу Rspec, авторское право 2010, стр. 228.
before(scope = :each, options={}, &block)
я использую before(:each)
заполнить некоторые данные для каждой группы примеров без необходимости вызывать let
метод для создания данных в блоке "it". Меньше кода в блоке "it" в этом случае.
я использую let
если я хочу некоторые данные в некоторых примерах, но не другие.
И before, и let отлично подходят для сушки блоков "it".
Чтобы избежать путаницы, "пусть" - это не то же самое, что before(:all)
, "Let" переоценивает свой метод и значение для каждого примера ("it"), но кэширует значение по нескольким вызовам в одном и том же примере. Вы можете прочитать больше об этом здесь: https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let
Примечание для Джозефа - если вы создаете объекты базы данных в before(:all)
они не будут зафиксированы в транзакции, и вы с большей вероятностью оставите в своей тестовой базе данных "кражу". использование before(:each)
вместо.
Другая причина использовать let и его ленивую оценку заключается в том, что вы можете взять сложный объект и протестировать отдельные части, переопределив let в контекстах, как в этом очень надуманном примере:
context "foo" do
let(:params) do
{ :foo => foo, :bar => "bar" }
end
let(:foo) { "foo" }
it "is set to foo" do
params[:foo].should eq("foo")
end
context "when foo is bar" do
let(:foo) { "bar" }
# NOTE we didn't have to redefine params entirely!
it "is set to bar" do
params[:foo].should eq("bar")
end
end
end
Я использую let
чтобы проверить мои ответы HTTP 404 в моих спецификациях API, используя контексты.
Для создания ресурса я использую let!
, Но для хранения идентификатора ресурса я использую let
, Посмотрите, как это выглядит:
let!(:country) { create(:country) }
let(:country_id) { country.id }
before { get "api/countries/#{country_id}" }
it 'responds with HTTP 200' { should respond_with(200) }
context 'when the country does not exist' do
let(:country_id) { -1 }
it 'responds with HTTP 404' { should respond_with(404) }
end
Это сохраняет спецификации чистыми и читаемыми.