128

http://betterspecs.org/#subject has some info about subject and let. However, I am still unclear on the difference between them. Furthermore, the SO post What is the argument against using before, let and subject in RSpec tests? said it is better to not use either subject or let. Where shall I go? I am so confused.

Community
  • 1
  • 1
new2cpp
  • 3,311
  • 5
  • 28
  • 39

4 Answers4

281

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
Dave Schweisguth
  • 36,475
  • 10
  • 98
  • 121
  • 4
    Also, if you want your multi-expectation blocks to run all expect lines (instead of not running if the first fails) you can use the `:aggregate_failures` tag in a line like `it ​"marks a task complete"​, ​:aggregate_failures​ ​do` (taken from the book Rails 5 Test Prescriptions) – labyrinth Sep 04 '18 at 19:56
  • 2
    I'm all against hypes and fetishes too, "single-expectation-per-example" is mainly for those who likes to group many unrelated expectations in a single example. Semantically speaking, your example is not the ideal, because it does not validate for a single digit number, it validates for real numbers in [0, 9] - which, surprise surprise, could be coded in a far more readable one single expectation `expect(result).to be_between(0, 9)`. – Andre Figueiredo Apr 21 '19 at 18:01
  • If you want to validates only for single digit number (Int [0,9]) it would be more suitable to do `expect(result.to_s).to match(/^[0-9]$/)` - I know it's ugly but it really test what you are saying, or perhaps, use `between` + `is_a? Integer`, but here you are testing the type too. And just `let`.. it should not be object of concern, and it's actually may be better to reevaluate values between examples. Otherwise +1 for the post – Andre Figueiredo Apr 21 '19 at 18:30
  • https://www.betterspecs.org/#subject recommends using the implicit subject, it makes tests easier to read, and let's be honest – rails is all about "magic". – Ryan Taylor Jul 24 '23 at 16:51
8

Subject and let are just tools to help you tidy up and speed up your tests. People in the rspec community do use them so i wouldn't worry about whether it's ok to use them or not. They can be used similarly but serve slightly different purposes

Subject allows you to declare a test subject, and then reuse it for any number of following test cases afterward. This reduces code repetition (DRYing up your code)

Let is an alternative to before: each blocks, which assign test data to instance variables. Let gives you a couple of advantages. First, it caches the value without assigning it to an instance variable. Second, it is lazily evaluated, which means that it doesn't get evaluated until a spec calls for it. Thus let helps you speed up your tests. I also think let is easier to read

Ren
  • 1,379
  • 2
  • 12
  • 24
6

subject is what is under test, usually an instance or a class. let is for assigning variables in your tests, which are evaluated lazily vs. using instance variables. There are some nice examples in this thread.

https://github.com/reachlocal/rspec-style-guide/issues/6

nikkypx
  • 1,925
  • 17
  • 12
0

They have almost identical implementations

Subject and let have different semantic meanings, but they have almost identical implementations. Subject, however, can be implicit:

# These are all equivalent
it { should ____ }
it { is_expected.to ____ }
it { expect(subject).to ____ }

Subject is what you're testing.

Let defines objects that will be used in the tests (or test setup etc.)

let! will not be run lazily, same is true of subject!. You can use these to create records etc. even if the variable isn't going to be explicitly referenced.

let!(:post) { create :post, user: subject }
subject { create :user }
it { should be_valid } # subject.valid? should be true
it { should have_attributes(posts?: true) } # subject.posts? should be true

Although, Dan has some reasons above, he is in the minority in considering using the implicit subject to be a bad practice. (https://www.betterspecs.org/#subject). Rails is all about magic, and the "single-expectation-per-example fetish" has the benefit of showing exactly what's failed.

They should be used to define objects, not call methods

Subject and let should both be objects, typically not method calls. They are lazily evaluated, but also memoized, and will only be run once per example.

The one exception that I use

I, personally, will sometimes break the above rule ONLY when doing eg.

# This is the EXCEPTION not the rule.
# Typically subject should be an object, like let,
# NOT something with side-effects.
subject do
  post posts_path(title: "Example", body: "Example post body")
end
it { expect { subject }.to change(Post, :count).by(1) }
it { expect { subject }.to change(User, :count).by(0) }

Which takes advantage of the fact that even subject is lazy.

Ryan Taylor
  • 12,559
  • 2
  • 39
  • 34