Когда использовать RSpec let ()?

448

Я склонен использовать перед блоками для установки переменных экземпляра. Затем я использую эти переменные в своих примерах. Я недавно наткнулся let(). Согласно документации RSpec, он используется для

... чтобы определить запомненный вспомогательный метод. Значение будет кэшировано для нескольких вызовов в одном и том же примере, но не во всех.

Чем это отличается от использования переменных экземпляра в до блоков? А также, когда вы должны использовать let()против before()?

Sent-Хил
источник
1
Пусть блоки лениво оцениваются, пока перед каждым примером бегут блоки (они в целом медленнее). Использование блоков before зависит от личных предпочтений (стиль кодирования, макеты / заглушки ...). Пусть блоки обычно предпочтительнее. Вы можете проверить более подробную информацию о let
Nesha Zoric
Не рекомендуется устанавливать переменные экземпляра в хуке before. Проверьте betterspecs.org
Эллисон

Ответы:

605

Я всегда предпочитаю letпеременную экземпляра по нескольким причинам:

  • Переменные экземпляра возникают при наличии ссылки. Это означает, что если вы нажмете правописание переменной экземпляра, будет создана и инициализирована новая nil, что может привести к незначительным ошибкам и ложным срабатываниям. Так как letсоздает метод, вы получите, NameErrorкогда вы неправильно его напишите, что я считаю предпочтительным. Это также облегчает рефакторинг спецификаций.
  • before(:each)Крючок будет работать перед каждым , например, даже если пример не использует какой - либо из переменных экземпляра , определенных в крюке. Обычно это не имеет большого значения, но если установка переменной экземпляра занимает много времени, то вы тратите впустую циклы. Для метода, определенного параметром let, код инициализации выполняется только в том случае, если его вызывает пример.
  • Вы можете выполнить рефакторинг из локальной переменной в примере непосредственно в let без изменения синтаксиса ссылок в примере. Если вы реорганизуете переменную экземпляра, вы должны изменить способ ссылки на объект в примере (например, добавить @).
  • Это немного субъективно, но, как отметил Майк Льюис, я думаю, что это облегчает чтение спецификации. Мне нравится организация определения всех моих зависимых объектов с letсохранением моего itблока красивым и коротким.

Связанную ссылку можно найти здесь: http://www.betterspecs.org/#let

Мирон Марстон
источник
2
Мне действительно нравится первое упомянутое вами преимущество, но не могли бы вы объяснить немного больше о третьем? До сих пор в примерах, которые я видел (спецификации mongoid: github.com/mongoid/mongoid/blob/master/spec/functional/mongoid/… ), используются однострочные блоки, и я не вижу, как отсутствие «@» делает это легче читать.
отправлено-хил
6
Как я уже сказал, это немного субъективно, но я считаю полезным использовать его letдля определения всех зависимых объектов и before(:each)для настройки необходимой конфигурации или любых макетов / заглушек, необходимых в примерах. Я предпочитаю это одному большому крючку, содержащему все это. Кроме того, let(:foo) { Foo.new }менее шумно (и более точно) before(:each) { @foo = Foo.new }. Вот пример того, как я его использую: github.com/myronmarston/vcr/blob/v1.7.0/spec/vcr/util/…
Myron Marston
Спасибо за пример, который действительно помог.
отправлено до
3
Эндрю Гримм: верно, но предупреждения могут генерировать тонны шума (т. Е. Из драгоценных камней, которые вы используете, которые не работают без предупреждений). Плюс, я предпочитаю получать NoMethodErrorпредупреждение, но YMMV.
Мирон Марстон
1
@ Jwan622: вы можете начать с написания одного примера, в котором есть foo = Foo.new(...)пользователи, а затем пользователи foo. Позже вы напишите новый пример в той же группе примеров, который также должен быть создан Fooтаким же образом. На этом этапе вы хотите провести рефакторинг для устранения дублирования. Вы можете удалить foo = Foo.new(...)строки из ваших примеров и заменить их let(:foo) { Foo.new(...) }без изменения способа использования примеров foo. Но если вы сделаете рефакторинг, before { @foo = Foo.new(...) }вам также придется обновить ссылки в примерах с fooна @foo.
Мирон Марстон,
82

Разница между использованием экземплярами переменных и в let()том , что let()является ленивой-оценено . Это означает, что let()он не оценивается, пока метод, который он определяет, не будет запущен в первый раз.

Разница между beforeи в letтом, что это let()дает вам хороший способ определения группы переменных в «каскадном» стиле. При этом спецификация выглядит немного лучше за счет упрощения кода.

Майк Льюис
источник
1
Я вижу, это действительно преимущество? Код выполняется для каждого примера независимо.
отправлено-хил
2
ИМО легче читать, а читаемость является огромным фактором для языков программирования.
Майк Льюис
4
Senthil - это на самом деле не обязательно запускаться в каждом примере, когда вы используете let (). Это ленивый, поэтому он запускается только если на него ссылаются. Вообще говоря, это не имеет большого значения, потому что целью группы примеров является запуск нескольких примеров в общем контексте.
Давид Челимский
1
Значит ли это, что вы не должны использовать, letесли вам нужно что-то оценивать каждый раз? Например, мне нужно, чтобы дочерняя модель присутствовала в базе данных, прежде чем какое-либо поведение будет запущено в родительской модели. Я не обязательно ссылаюсь на эту дочернюю модель в тесте, потому что я тестирую поведение родительских моделей. В данный момент я использую let!метод вместо этого, но, возможно, было бы более явным добавить эту настройку before(:each)?
гар
2
@gar - я бы использовал Factory (например, FactoryGirl), которая позволяет вам создавать необходимые дочерние ассоциации при создании экземпляра родителя. Если вы делаете это таким образом, то не имеет значения, используете ли вы let () или блок установки. Функция let () хороша, если вам не нужно использовать ВСЕ для каждого теста в вашем под-контексте. Настройка должна иметь только то, что требуется для каждого.
Harmon
17

Я полностью заменил все использования переменных экземпляра в моих тестах 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-втор-пусть-в-перед тем , блоки

Хо-Шэн Сяо
источник
Эй, Хо-Шен, я прочитал несколько твоих постов в блоге, прежде чем задавать этот вопрос. Что касается вашего # spec/friendship_spec.rbи # spec/comment_spec.rbпримера, вы не думаете, что они делают его менее читабельным? Я понятия не имею, откуда usersвзяться, и нужно будет копать глубже.
отправлено до
Первые дюжины или около того людей, которым я показал формат, все находят, что он гораздо более читабелен, и некоторые из них начали писать с ним. Теперь у меня достаточно спецификационного кода с использованием let (), и я столкнулся с некоторыми из этих проблем. Я обнаруживаю, что иду к примеру и, начиная с самой внутренней группы примеров, снова работаю. Это тот же навык, что и использование метапрограммируемой среды.
Хо-Шэн Сяо
2
Самая большая проблема, с которой я столкнулся - это случайно использовать let (: subject) {} вместо subject {}. subject () настроен не так, как let (: subject), но пусть (: subject) переопределит его.
Хо-Шэн Сяо
1
Если вы можете «углубиться» в код, то вы обнаружите, что сканирование кода с помощью объявлений let () происходит намного быстрее. При сканировании кода легче выбрать объявления let (), чем найти @variables, встроенные в код. Используя @variables, у меня нет хорошей «формы», для которой строки относятся к назначению переменных, а какие строки относятся к тестированию переменных. Используя let (), все присваивания выполняются с помощью let (), поэтому вы «мгновенно узнаете» по форме букв, в которых находятся ваши объявления.
Хо-Шэн Сяо
1
Вы можете сделать один и тот же аргумент о том, что переменные экземпляра легче выбрать, тем более что некоторые редакторы, такие как mine (gedit), выделяют переменные экземпляра. Я использовал let()последние пару дней, и лично я не вижу разницы, за исключением первого упомянутого преимущества Мирон. И я не уверен в том, чтобы отпустить, а что нет, может быть, потому что я ленивый и мне нравится смотреть код заранее, не открывая еще один файл. Спасибо за ваши комментарии.
отправлено-Hil
13

Важно помнить, что let оценивается лениво и не содержит в себе методов побочных эффектов, иначе вы не сможете легко перейти от let к before (: каждому) . Вы можете использовать пусть! вместо того, чтобы позволить так, чтобы это оценивалось перед каждым сценарием.

Писарук
источник
8

В целом, let()это более приятный синтаксис, и он экономит на наборе @nameсимволов повсюду. Но будьте бдительны! Я let()также обнаружил, что вводит тонкие ошибки (или, по крайней мере, царапины на голове), потому что переменная на самом деле не существует, пока вы не попытаетесь ее использовать ... Скажите знак сказки: если добавление putsпосле, let()чтобы увидеть, что переменная верна, позволяет спецификации пройти, но без putsспецификации не удается - вы нашли эту тонкость.

Я также обнаружил, что let(), похоже, не кэшируется при любых обстоятельствах! Я написал это в своем блоге: http://technicaldebt.com/?p=1242

Может это только у меня?

Джон Керн
источник
9
letвсегда запоминает значение на время одного примера. Он не запоминает значение в нескольких примерах. before(:all)напротив, позволяет повторно использовать инициализированную переменную в нескольких примерах.
Мирон Марстон
2
если вы хотите использовать let (как сейчас кажется наилучшей практикой), но вам нужна конкретная переменная, которую нужно создать сразу же, это то, для чего let!она предназначена. relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/…
Джейкоб
6

пусть является функциональным, так как по сути это 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}
engineerDave
источник
2

Примечание для Джозефа - если вы создаете объекты базы данных в 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
dotdotdotPaul
источник
1
+1 до (: все) ошибки потратили много дней нашего времени разработчиков.
Майкл Даррант
1

«до» по умолчанию подразумевается before(:each). Ссылка на книгу Rspec, авторское право 2010, стр. 228.

before(scope = :each, options={}, &block)

Я использую before(:each)для заполнения некоторые данные для каждой группы примеров без необходимости вызывать letметод для создания данных в блоке «it». В этом случае меньше кода в блоке "it".

Я использую, letесли я хочу некоторые данные в некоторых примерах, но не другие.

И до, и пусть отлично подходят для сушки блоков "it".

Чтобы избежать путаницы, «пусть» - это не то же самое, что before(:all). «Let» переоценивает свой метод и значение для каждого примера («it»), но кэширует значение по нескольким вызовам в одном и том же примере. Вы можете прочитать больше об этом здесь: https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let

konyak
источник
1

Здесь несогласный голос: после 5 лет rspec мне не letочень нравится .

1. Ленивая оценка часто сбивает с толку настройку теста

Становится трудно рассуждать о настройке, когда некоторые вещи, которые были объявлены в настройке, фактически не влияют на состояние, в то время как другие.

В конце концов, из-за разочарования кто-то просто переключается letна let!(то же самое без ленивой оценки), чтобы заставить их спецификацию работать. Если это работает для них, рождается новая привычка: когда новая спецификация добавляется в более старый набор, и она не работает, первое , что пытается сделать писатель, - это добавлять удары в случайные letвызовы.

Довольно скоро все преимущества производительности исчезли.

2. Специальный синтаксис необычен для пользователей не rspec

Я бы лучше научил Ruby моей команде, чем уловкам rspec. Переменные экземпляра или вызовы методов полезны везде в этом и других проектах, letсинтаксис будет полезен только в rspec.

3. «Преимущества» позволяют нам легко игнорировать хорошие изменения дизайна

let()хорош для дорогих зависимостей, которые мы не хотим создавать снова и снова. Это также хорошо сочетается сsubject , позволяя вам высушить повторные вызовы методов с несколькими аргументами

Дорогие зависимости повторялись много раз, и методы с большими сигнатурами - это те точки, где мы могли бы улучшить код:

  • может быть, я смогу представить новую абстракцию, которая изолирует зависимость от остальной части моего кода (что означало бы, что это нужно меньшему количеству тестов)
  • может быть, тестируемый код делает слишком много
  • может быть, мне нужно вводить более умные объекты вместо длинного списка примитивов
  • может быть, у меня есть нарушение сказать-не-спросить
  • может быть, дорогой код можно сделать быстрее (реже - остерегайтесь преждевременной оптимизации здесь)

Во всех этих случаях я могу устранить симптомы сложных тестов с помощью успокаивающего бальзама с магией rspec или попытаться устранить причину. Я чувствую, что последние несколько лет я потратил слишком много времени на первое, и теперь я хочу немного лучшего кода.

Чтобы ответить на оригинальный вопрос: я бы предпочел не делать этого, но я все еще использую let. я основном использую его, чтобы соответствовать стилю остальной части команды (кажется, что большинство программистов на Rails в мире сейчас глубоко знакомы с магией rspec, что происходит очень часто). Иногда я использую его, когда добавляю тест в какой-то код, который я не могу контролировать или у которого нет времени на рефакторинг для лучшей абстракции: то есть, когда единственным вариантом является болеутоляющее средство.

iftheshoefritz
источник
0

Я использую 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

Это сохраняет спецификации чистыми и читаемыми.

Виниций Бразилия
источник