Коли використовувати RSpec let ()?


448

Я схильний використовувати перед блоками для встановлення змінних екземплярів. Потім я використовую ці змінні на своїх прикладах. Я нещодавно натрапив let(). Згідно з документами RSpec, вона звикла

... визначити запам’ятований метод помічника. Значення буде кешоване в декількох викликах в одному прикладі, але не в прикладах.

Чим це відрізняється від використання змінних екземплярів у блоках перед блоками? А також коли слід використовувати let()vs before()?


1
Нехай блоки ліниво оцінюються, тоді як перед блоками виконуються перед кожним прикладом (вони в цілому повільніше). Використання перед блоками залежить від особистих переваг (стиль кодування, макети / заглушки ...). Нехай блоки є переважно переважними. Ви можете ознайомитись з більш детальною інформацією про let
Неша Зорич

Недоцільно встановлювати змінні екземпляри в гачку перед. Ознайомтеся з betterspecs.org
Елісон

Відповіді:


605

Я завжди віддаю перевагу letзмінній екземпляру з кількох причин:

  • Змінні інстанції виникають при посиланні. Це означає, що якщо ви жирним пальцем написання змінної екземпляра, буде створена та ініціалізована нова nil, що може призвести до тонких помилок та помилкових позитивних результатів. Оскільки letстворюється метод, ви отримаєте NameErrorпомилку, коли його неправильно написати, що я вважаю кращим. Це також спрощує рефакторні характеристики.
  • before(:each)Гачок буде працювати перед кожним , наприклад, навіть якщо приклад не використовує який - або з змінних екземпляра , визначених у гаку. Зазвичай це не велика справа, але якщо налаштування змінної екземпляра займає тривалий час, ви витрачаєте цикли. Для методу, визначеного letкодом, ініціалізаційний код запускається лише у тому випадку, коли його приводить приклад.
  • Ви можете рефактор з локальної змінної в прикладі безпосередньо в let, не змінюючи синтаксис референції в прикладі. Якщо ви перетворюєте на змінну екземпляра, вам потрібно змінити спосіб посилання на об'єкт у прикладі (наприклад, додати@ ).
  • Це трохи суб’єктивно, але, як зазначав Майк Льюїс, я думаю, що це полегшує читання специфікації. Мені подобається організація визначення всіх моїх залежних об'єктів letі збереження мого itблоку красивим і коротким.

Пов'язане посилання можна знайти тут: http://www.betterspecs.org/#let


2
Мені дуже подобається перша перевага, яку ви згадали, але ви могли б пояснити трохи більше про третю? Поки що приклади, які я бачив (монгоїдні характеристики: 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/…
Майрон Марстон

Дякую за приклад, що справді допомогло.
відправлено-гору

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

Як сказано в деяких інших відповідях тут, хай () ліниво оцінюється, тому він завантажує лише ті, які потребують завантаження. Це ПУСКАТЬ спекуляцію та зробить її більш зрозумілою. Я фактично переніс код Rspec let (), який потрібно використовувати у своїх контролерах, у стилі Спадщина_resource gem. http://ruby-lambda.blogspot.com/2010/06/stealing-let-from-rspec.html

Поряд із лінивою оцінкою, ще одна перевага полягає в тому, що в поєднанні з ActiveSupport :: Concern та специфікацією навантаження / підтримкою / поведінкою ви можете створити свій власний специфікаційний mini-DSL, специфічний для вашої програми. Я написав їх для тестування на Rack та RESTful ресурси.

Я використовую стратегію "Фабрично все" (через Machinist + Forgery / Faker). Однак можна використовувати його в поєднанні з попередніми блоками (: кожен) для попереднього завантаження заводів для цілого набору груп прикладів, що дозволяє запускати технічні характеристики швидше: http://makandra.com/notes/770-taking-advantage -of-rspec-s-пускаємо в-перед-блоки


Привіт Хо-Шен, я насправді прочитав кілька ваших публікацій у блозі, перш ніж задавати це питання. Що стосується вашого # spec/friendship_spec.rbта # spec/comment_spec.rbприкладу, ви не вважаєте, що вони роблять його менш читабельним? Я не маю уявлення, звідки usersберуться і потрібно буде копати глибше.
надіслано-hil

Перший десяток або близько того людей, яким я показав формат, вважають його набагато більш читабельним, і деякі з них почали писати з ним. Зараз у мене достатньо специфікаційного коду, використовуючи let (), що я також стикаюся з деякими з цих проблем. Мені здається, що я йду на приклад, і, починаючи з найпотужнішої прикладної групи, працюю над тим, щоб зробити резервну копію. Це та сама майстерність, як використання високопрограмованого середовища.
Ho-Sheng Hsiao

2
Найбільша проблема, на яку я зіткнувся, - випадково використання let (: subject) {} замість теми {}. тема () налаштована інакше, ніж let (: subject), але нехай (: subject) замінить її.
Хо-Шен Сяо

1
Якщо ви можете відпустити "свердління" в код, ви знайдете сканування коду з деклараціями let () набагато швидше. Під час сканування коду легше вибрати декларації let (), ніж знайти вбудований у код @variables. Використовуючи @variables, я не маю гарної "форми", для якої рядки посилаються на присвоєння змінним, а які - на тестування змінних. Використовуючи let (), всі завдання виконуються з let (), так що ви знаєте "миттєво" за формою літер, де знаходяться ваші декларації.
Хо-Шен Сяо

1
Ви можете зробити той самий аргумент про те, що змінні екземплярів простіше виділити, тим більше, що деякі редактори, як-от моя (gedit), виділяють змінні екземплярів. Я використовував let()останні кілька днів, і особисто я не бачу різниці, окрім першої переваги, про яку згадав Майрон. І я не дуже впевнений у тому, щоб відпустити, а що ні, можливо, тому що я лінивий і мені подобається бачити код наперед, не відкриваючи ще один файл. Дякуємо за ваші коментарі.
надіслано-hil

13

Важливо пам’ятати, що дозволено ліниво оцінюватись і не вводити в неї побічних ефектів, інакше ви не зможете легко змінити з дозволеного на (до кожного) . Ви можете використовувати let! замість того, щоб не так, щоб вона оцінювалася перед кожним сценарієм.


8

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

Я також виявив, що let(), схоже, не зберігається в будь-яких обставинах! Я написав це у своєму блозі: http : //techničkedebt.com/?p=1242

Може, це тільки я?


9
letзавжди запам'ятовує значення протягом тривалості одного прикладу. Це не запам'ятовує значення на кількох прикладах. before(:all)навпаки, дозволяє повторно використовувати ініціалізовану змінну у кількох прикладах.
Майрон Марстон

2
якщо ви хочете використовувати let (як зараз, здається, вважається найкращою практикою), але вам потрібна одразу ж конкретна змінна, щоб миттєво познайомитися, саме для let!цього призначено. relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/…
Яків

6

нехай функціональний, як по суті, Proc. Також її кешування.

Один готч я знайшов одразу з дозволеним ... У блоці Spec, який оцінює зміни.

let(:object) {FactoryGirl.create :object}

expect {
  post :destroy, id: review.id
}.to change(Object, :count).by(-1)

Вам потрібно буде обов'язково зателефонувати letза межами очікуваного блоку. тобто ви телефонуєте FactoryGirl.createу своєму блоці дозволу. Зазвичай я це роблю, перевіряючи, чи зберігається об'єкт.

object.persisted?.should eq true

Інакше при letпершому виклику блоку зміни бази даних насправді відбудуться через ледачу інстанцію.

Оновлення

Просто додавання примітки. Будьте обережні, граючи в гольф-код або в цьому випадку rspec golf з цією відповіддю.

У цьому випадку я просто повинен викликати якийсь метод, на який об’єкт відповідає. Тому я закликаю _.persisted?метод _ на об'єкт як його правду. Все, що я намагаюся зробити, це інстанціювати об'єкт. Ви могли б зателефонувати порожнім? чи нуль? теж. Суть не в тесті, а в тому, щоб повернути об'єкт до життя, зателефонувавши йому.

Таким чином, ви не можете рефактор

object.persisted?.should eq true

бути

object.should be_persisted 

як об'єкт не був примірник ... його ледачий. :)

Оновлення 2

використовувати лей! синтаксис для миттєвого створення об'єкта, що має взагалі уникати цього питання. Зауважте, хоча це переможе чимало мети лінощі незворушеного хай.

Також у деяких випадках ви, можливо, хочете використовувати синтаксис теми, а не дозволяти, оскільки це може дати вам додаткові можливості.

subject(:object) {FactoryGirl.create :object}

2

Зауважте Джозефу - якщо ви створюєте об'єкти бази даних у before(:all)них, вони не будуть захоплені транзакцією, і ви набагато більше шансів залишити суто в тестовій базі даних. Використовуйтеbefore(:each) замість цього.

Інша причина використання 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

1
+1 раніше (: всі) помилки витратили багато днів нашого часу для розробників.
Майкл Дюррант

1

"до" за замовчуванням означає before(:each). Ref Книга Rspec, авторські права 2010, стор. 228.

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

Я використовую before(:each)для виведення деяких даних для кожної прикладної групи, не вимагаючи виклику letметоду створення даних у блоці "це". У цьому випадку менше коду в блоці "it".

Я використовую, letякщо хочу отримати деякі дані в деяких прикладах, а не в інших.

І раніше, і нехай чудово підходять для висушування блоків "це".

Щоб уникнути плутанини, "нехай" - це не те саме, що before(:all). "Нехай" повторно оцінює його метод та значення для кожного прикладу ("це"), але кешує значення для кількох викликів у тому самому прикладі. Більше про це можна прочитати тут: https://www.relishapp.com/rspec/rspec-core/v/2-6/docs/helper-methods/let-and-let


1

Голос непристойного тут: після 5 років rspec мені не letдуже подобається .

1. Ледача оцінка часто робить налаштування тесту заплутаним

Важко міркувати про налаштування, коли деякі речі, які були задекларовані у налаштуваннях, насправді не впливають на стан, а інші є.

Зрештою, від розчарування хтось просто переходить letна let!(те ж саме, без лінивої оцінки), щоб їх специфікація працювала. Якщо це для них спрацює, народиться нова звичка: коли до старшого набору додається нова специфікація, і вона не працює, перше , що намагається письменник, - це додати чубок до випадкових випадківlet дзвінків.

Досить скоро всі переваги від продуктивності втрачаються.

2. Спеціальний синтаксис незвичний для користувачів, що не належать до rspec

Я б швидше навчив Рубі своїй команді, ніж хитрості rspec. Змінні екземпляри або виклики методів корисні скрізь у цьому проекті та інших, letсинтаксис буде корисним лише у rspec.

3. "Переваги" дозволяють нам легко ігнорувати хороші зміни дизайну

let()добре для дорогих залежностей, які ми не хочемо створювати знову і знову. Він також добре поєднується зsubject , що дозволяє висушити повторні дзвінки до методів, що багато аргументів

Дорогі залежності повторюються багато разів, а методи з великими підписами - це обидва пункти, де ми могли б покращити код:

  • можливо я можу ввести нову абстракцію, яка виділяє залежність від решти мого коду (це означало б, що менша кількість тестів потребує цього)
  • можливо, тестовий код робить занадто багато
  • можливо, мені потрібно ввести розумніші об’єкти замість довгого списку примітивів
  • можливо, у мене є порушення скажи-не запитай
  • можливо, дорогий код можна зробити швидше (рідше - остерігайтесь тут передчасної оптимізації)

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

Щоб відповісти на початкове запитання: я вважаю за краще не робити, але все одно користуюся let. Я здебільшого використовую його, щоб відповідати стилю решти команди (схоже, більшість програмістів Rails у світі зараз заглиблюються у свою магію rspec, так що це дуже часто). Іноді я використовую його, коли додаю тест до якогось коду, який я не маю під контролем, або не маю часу переробляти на кращу абстракцію: тобто коли єдиним варіантом є знеболювальне.


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

Це забезпечує технічні характеристики чистими і читабельними.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.