Когда использовать 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

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

Другие вопросы по тегам