ChatGPT解决这个技术问题 Extra ChatGPT

RSpec: What is the difference between let and a before block?

What is difference between let and a before block in RSpec?

And when to use each?

What will be good approach (let or before) in below example?

let(:user) { User.make !}
let(:account) {user.account.make!}

before(:each) do
 @user = User.make!
 @account = @user.account.make!
end

I studied this stackoverflow post

But is it good to define let for association stuff like above?

Basically, 'let' is used by people who dislike instance variables. As a side note, you should consider using FactoryGirl or a similar tool.

J
Jay

People seem to have explained some of the basic ways in which they differ, but left out before(:all) and don't explain exactly why they should be used.

It's my belief that instance variables have no place being used in the vast majority of specs, partly due to the reasons mentioned in this answer, so I won't mention them as an option here.

let blocks

Code within a let block is only executed when referenced, lazy loading this means that ordering of these blocks is irrelevant. This gives you a large amount of power to cut down on repeated setup through your specs.

One (extremely small and contrived) example of this is:

let(:person)     { build(:person) }
subject(:result) { Library.calculate_awesome(person, has_moustache) }

context 'with a moustache' do
  let(:has_moustache) { true } 
  its(:awesome?)      { should be_true }
end

context 'without a moustache' do
  let(:has_moustache) { false } 
  its(:awesome?)      { should be_false }
end

You can see that has_moustache is defined differently in each case, but there's no need to repeat the subject definition. Something important to note is that the last let block defined in the current context will be used. This is good for setting a default to be used for the majority of specs, which can be overwritten if needed.

For instance, checking the return value of calculate_awesome if passed a person model with top_hat set to true, but no moustache would be:

context 'without a moustache but with a top hat' do
  let(:has_moustache) { false } 
  let(:person)        { build(:person, top_hat: true) }
  its(:awesome?)      { should be_true }
end

Another thing to note about let blocks, they should not be used if you're searching for something which has been saved to the database (i.e. Library.find_awesome_people(search_criteria)) as they will not be saved to the database unless they have already been referenced. let! or before blocks are what should be used here.

Also, never ever use before to trigger execution of let blocks, this is what let! is made for!

let! blocks

let! blocks are executed in the order they are defined (much like a before block). The one core difference to before blocks is that you get an explicit reference to this variable, rather than needing to fall back to instance variables.

As with let blocks, if multiple let! blocks are defined with the same name, the most recent is what will be used in execution. The core difference being that let! blocks will be executed multiple times if used like this, whereas the let block will only execute the last time.

before(:each) blocks

before(:each) is the default before block, and can therefore be referenced as before {} rather than specifying the full before(:each) {} each time.

It's my personal preference to use before blocks in a few core situations. I will use before blocks if:

I'm using mocking, stubbing or doubles

There is any reasonable sized setup (generally this is a sign your factory traits haven't been setup correctly)

There's a number of variables which I don't need to reference directly, but are required for setup

I'm writing functional controller tests in rails, and I want to execute a specific request to test (i.e. before { get :index }). Even though you could use subject for this in a lot of cases, it sometimes feels more explicit if you don't require a reference.

If you find yourself writing large before blocks for your specs, check your factories and make sure you fully understand traits and their flexibility.

before(:all) blocks

These are only ever executed once, before the specs in the current context (and its children). These can be used to great advantage if written correctly, as there are certain situations this can cut down on execution and effort.

One example (which would hardly affect execution time at all) is mocking out an ENV variable for a test, which you should only ever need to do once.

Hope that helps :)


For minitest users, which I am but I did not notice the RSpec tag of this Q&A, the before(:all) option does not exist in Minitest. Here are some workaround in the comments: github.com/seattlerb/minitest/issues/61
Article referenced is a dead link :\
Thanks @DylanPierce. I can't find any copies of that article, so I've referenced a SO answer which addresses this instead :)
Thanks :) much appreciated
its is no longer in rspec-core. A more modern, idiomatic way is it { is_expected.to be_awesome }.
J
JJD

Almost always, I prefer let. The post that you link specifies that let is also faster. However, at times, when many commands have to be executed, I could use before(:each) because its syntax is more clear when many commands are involved.

In your example, I would definitely prefer to use let instead of before(:each). Generally speaking, when just some variable initialization is done, I tend to like using let.


m
mikeweber

A big difference that hasn't been mentioned is that variables defined with let don't get instantiated until you call it the first time. So while a before(:each) block would instantiate all of the variables, let let's you define a number of variables you might use across multiple tests, it doesn't automatically instantiate them. Without knowing this, your tests could come back to bite yourself if you're expecting all of the data to be loaded beforehand. In some cases, you may even want to define a number of let variables, then use a before(:each) block to call each let instance just to make sure the data is available to begin with.


You can use let! to define methods that are called before each example. See RSpec docs.
T
Tim Connor

It looks like you are using Machinist. Beware, you may see some issues with make! inside of let (the non-bang version) happening outside of the global fixture transaction (if you are using transactional fixtures as well) so corrupting the data for your other tests.