What Is the Argument Against Using Before, Let and Subject in Rspec Tests

What is the argument against using before, let and subject in RSpec tests?

Interesting question; something that I want to know more about as well.... So dug in a bit, and here is what I uncovered:

Thoughtbot style guide dictum about let, etc.

  1. In an earlier version of the style guide, there's more to that statement:

    Avoid its, let, let!, specify, subject, and other DSLs. Prefer explicitness and consistency.

  2. ThoughtBot folks made a post on let name let's not. See also the link to the Github Commit Comment conversation

  3. On one of their recent podcasts Joe Ferris, the CTO of ThoughtBot, explains why it's not a good idea to use let and subject. Check out the podcast titled Something Else Was Smellier from the 27m37s mark onwards for the next 5 minutes.

  4. Testing Anti-pattern 'Mystery Guest' which is dealt with in detail in an older ThoughtBot blogpost is the main reason for why not to use let and its cousins.

To summarize my understanding of all the above very succinctly:

Using let et al makes it difficult to understand what's happening
within the test on a quick glance, and requires the person to spend
some time in making the connections.

Write tests such that it is easy to understand without much effort.

In addition, using let liberally in tests results in over-sharing among the tests, as well as makes implicit common fixtures - i.e. having a common fixture to start with for every test being written even when it does not apply.

before(:all)

The argument against using before(:all) is straight-forward. As explained in the old rspec documentation:

Warning: The use of before(:all) and after(:all) is generally discouraged because it introduces dependencies between the Examples. Still, it might prove useful for very expensive operations if you know what you are doing.

before(:all) gets executed only once at the start of the ExampleGroup. As such there is a potential to inadvertently introduce dependencies between the Examples. Thoughtbot's assertion about the tests not being easy to understand applies to this as well.

In conclusion, the advise for writing better specs seems to be:

  1. Write tests such that they are easy to understand on a quick glance.
  2. Know what you are doing.

What's the difference between RSpec's subject and let? When should they be used or not?

Summary: RSpec's subject is a special variable that refers to the object being tested. Expectations can be set on it implicitly, which supports one-line examples. It is clear to the reader in some idiomatic cases, but is otherwise hard to understand and should be avoided. RSpec's let variables are just lazily instantiated (memoized) variables. They aren't as hard to follow as the subject, but can still lead to tangled tests so should be used with discretion.

The subject

How it works

The subject is the object being tested. RSpec has an explicit idea of the subject. It may or may not be defined. If it is, RSpec can call methods on it without referring to it explicitly.

By default, if the first argument to an outermost example group (describe or context block) is a class, RSpec creates an instance of that class and assigns it to the subject. For example, the following passes:

class A
end

describe A do
it "is instantiated by RSpec" do
expect(subject).to be_an(A)
end
end

You can define the subject yourself with subject:

describe "anonymous subject" do
subject { A.new }
it "has been instantiated" do
expect(subject).to be_an(A)
end
end

You can give the subject a name when you define it:

describe "named subject" do
subject(:a) { A.new }
it "has been instantiated" do
expect(a).to be_an(A)
end
end

Even if you name the subject, you can still refer to it anonymously:

describe "named subject" do
subject(:a) { A.new }
it "has been instantiated" do
expect(subject).to be_an(A)
end
end

You can define more than one named subject. The most recently defined named subject is the anonymous subject.

However the subject is defined,

  1. It's instantiated lazily. That is, the implicit instantiation of the described class or the execution of the block passed to subject doesn't happen until subject or the named subject is referred to in an example. If you want your explict subject to be instantiated eagerly (before an example in its group runs), say subject! instead of subject.

  2. Expectations can be set on it implicitly (without writing subject or the name of a named subject):

    describe A do
    it { is_expected.to be_an(A) }
    end

    The subject exists to support this one-line syntax.

When to use it

An implicit subject (inferred from the example group) is hard to understand because

  • It's instantiated behind the scenes.
  • Whether it's used implicitly (by calling is_expected without an explicit receiver) or explicitly (as subject), it gives the reader no information about the role or nature of the object on which the expectation is being called.
  • The one-liner example syntax doesn't have an example description (the string argument to it in the normal example syntax), so the only information the reader has about the purpose of the example is the expectation itself.

Therefore, it's only helpful to use an implicit subject when the context is likely to be well understood by all readers and there is really no need for an example description. The canonical case is testing ActiveRecord validations with shoulda matchers:

describe Article do
it { is_expected.to validate_presence_of(:title) }
end

An explict anonymous subject (defined with subject without a name) is a little better, because the reader can see how it's instantiated, but

  • it can still put the instantiation of the subject far from where it's used (e.g. at the top of an example group with many examples that use it), which is still hard to follow, and
  • it has the other problems that the implicit subject does.

A named subject provides an intention-revealing name, but the only reason to use a named subject instead of a let variable is if you want to use the anonymous subject some of the time, and we just explained why the anonymous subject is hard to understand.

So, legitimate uses of an explicit anonymous subject or a named subject are very rare.

let variables

How they work

let variables are just like named subjects except for two differences:

  • they're defined with let/let! instead of subject/subject!
  • they do not set the anonymous subject or allow expectations to be called on it implicitly.

When to use them

It's completely legitimate to use let to reduce duplication among examples. However, do so only when it doesn't sacrifice test clarity. The safest time to use let is when the let variable's purpose is completely clear from its name (so that the reader doesn't have to find the definition, which could be many lines away, to understand each example) and it is used in the same way in every example. If either of those things isn't true, consider defining the object in a plain old local variable or calling a factory method right in the example.

let! is risky, because it's not lazy. If someone adds an example to the example group that contains the let!, but the example doesn't need the let! variable,

  • that example will be hard to understand, because the reader will see the let! variable and wonder whether and how it affects the example
  • the example will be slower than it needs to be, because of the time taken to create the let! variablle

So use let!, if at all, only in small, simple example groups where it's less likely that future example writers will fall into that trap.

The single-expectation-per-example fetish

There is a common overuse of subjects or let variables that's worth discussing separately. Some people like to use them like this:

describe 'Calculator' do
describe '#calculate' do
subject { Calculator.calculate }
it { is_expected.to be >= 0 }
it { is_expected.to be <= 9 }
end
end

(This is a simple example of a method that returns a number for which we need two expectations, but this style can have many more examples/expectations if the method returns a more complicated value that needs many expectations and/or has many side effects that all need expectations.)

People do this because they've heard that one should have only one expectation per example (which is mixed up with the valid rule that one should only test one method call per example) or because they're in love with RSpec trickiness. Don't do it, whether with an anonymous or named subject or a let variable! This style has several problems:

  • The anonymous subject isn't the subject of the examples — the method is the subject. Writing the test this way screws up the language, making it harder to think about.
  • As always with one-line examples, there isn't any room to explain the meaning of the expectations.
  • The subject has to be constructed for each example, which is slow.

Instead, write a single example:

describe 'Calculator' do
describe '#calculate' do
it "returns a single-digit number" do
result = Calculator.calculate
expect(result).to be >= 0
expect(result).to be <= 9
end
end
end

RSpec - If I avoid using let, let and before, how to achieve same functionality?

Whoever wrote those best practices actually wants to just say "don't use RSpec". It's insane to avoid using core features and still think you can get things done well. Can you link to these best practices? I'd love to read them and see if they're justified or just some j-random-guy's opinion.

Avoid before and let? What? Really?

Avoid specify, I guess, it's just older syntax for it.

Avoid a specific subject call? I guess, if your code lives in an idealistic fantasy land. Strive to minimize it and use implicit subjects? Sure.

Avoid its? It's awesome! Why avoid it? Self-documenting one-line specs? How horrible.

Avoid excessive rspec nesting with subject, let, and alternative arguments

How about you write it like this?
expect(subject.call(foo)) is not very pretty but it gets rid of the nesting.

describe "#some_method" do
subject { course.method(:some_method) }

it 'returns the one after if given a project' do
expect(subject.call(random[1])).to eq(random[2])
end

it 'returns nil when it is the last' do
expect(subject.call(random.last)).to be_nil
end

it 'returns nil...' do
expect(subject.call(something.else)).to be_nil
end
end

When to use RSpec let()?

I always prefer let to an instance variable for a couple of reasons:

  • Instance variables spring into existence when referenced. This means that if you fat finger the spelling of the instance variable, a new one will be created and initialized to nil, which can lead to subtle bugs and false positives. Since let creates a method, you'll get a NameError when you misspell it, which I find preferable. It makes it easier to refactor specs, too.
  • A before(:each) hook will run before each example, even if the example doesn't use any of the instance variables defined in the hook. This isn't usually a big deal, but if the setup of the instance variable takes a long time, then you're wasting cycles. For the method defined by let, the initialization code only runs if the example calls it.
  • You can refactor from a local variable in an example directly into a let without changing the
    referencing syntax in the example. If you refactor to an instance variable, you have to change
    how you reference the object in the example (e.g. add an @).
  • This is a bit subjective, but as Mike Lewis pointed out, I think it makes the spec easier to read. I like the organization of defining all my dependent objects with let and keeping my it block nice and short.

A related link can be found here: http://www.betterspecs.org/#let

Why Do not use `expect` in hooks such as `before`?

Four phase test is a testing pattern commonly used for unit tests. It's general form:

test do
setup
exercise
verify
teardown
end

before is part of the setup phase where the developer creates the scenario and supporting data.

expect is part of the verify phase, which happens inside a it block.

A common pattern is to use allow in before blocks and use expect in it blocks for example.

RSpec.describe User do
describe '#forgot_password' do
before { allow(EmailService).to receive(:send) }

subject { FactoryBot.create(:user).forgot_password }

it 'sends an email to user to start the password resetting process' do
subject
expect(EmailService).to have_received(:send).with(...)
end
end
end

before blocks can also be added in other layers of the application (spec_helper.rb, shared examples) and one does not want to rely on the correct order of before blocks in order for a test to be successful.

Avoid recreating the subject in RSpec

You can put that in to a before(:all) block. I don't know if that syntax has changed in a new rspec version, but regardless, your test would become this:

before(:all) do
response = get "/expensive_lookup"
@res = JSON.parse(response.body)
end

it "returns the right transaction ID" do
@res["transaction_id"].should == 1
end

# etc

The pro is that the code in the before-all block gets run just once for your spec. The con is that, as you can see, you can't take advantage of the subject; you need to write each more explicitly. Another gotcha is that any data saved to the test database is not part of the transaction and will not be rolled back.

Patterns for accessing subject hierarchy in RSpec

In this type of hierarchic structure, I would actually drop the use of subject and make the subject explicit. While this could result in a bit more typing (if you have a lot of tests), it is also clearer.
The nested subject-statements could also be confusing what is actually being tested if you are three levels down.

  context 'with a result which is a Hash' do
before do
@result = get_result()
end
it { @result.should be_a Hash }
context 'special_key' do
before do
@array_elem = @result[special_key]
end
it { @array_elem.should be_an Array }
context 'that contains a Hash' do
before do
@nested_hash = ...
end
it { @nested_hash.should be_a Hash }
...
end
end
end

But this could be a matter of taste.
Hope this helps.



Related Topics



Leave a reply



Submit