Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Difference between subject and let #6

Closed
odigity opened this issue Feb 2, 2015 · 9 comments
Closed

Difference between subject and let #6

odigity opened this issue Feb 2, 2015 · 9 comments

Comments

@odigity
Copy link

odigity commented Feb 2, 2015

I've spent the last half hour trying to google the difference between subject and let, after having already read the RSpec docs, which is how I came across this guide. Which I'm thankfully for, because there's lots of useful suggestions here.

However, when I finally got to the subject/lets section, my excitement turned to disappointment. I still don't understand the difference between subject and let...

Help?

@RyanHedges
Copy link
Contributor

subject is the thing under test. With RSpec, it will usually be an instance of the object/class you are testing.

class SomeClass
  def some_method
    puts "hello"
  end
end

instance_of_some_class = SomeClass.new
instance_of_some_class.some_method #=> returns puts "hello"

#using subject to write this test
describe SomeClass do
  describe 'some_method' do
    it 'prints out hello' do
      expect(subject.some_method).to eq("hello")
    end
  end
end

subject is often given to you as a nicety in RSpec. That's why you'll sometimes see it set to something else; people don't want to test an instance of the class but rather something else or more specific. You can think of it being the same as doing the following for the code above.

describe 'SomeClass' do
  before do
    @subject = SomeClass.new  #=> create a new instance of the class for the thing under test
  end
  describe 'some_method' do
    it 'prints out hello' do
      expect(@subject.some_method).to eq("hello")
    end
  end
end

let is a way for you to create instance variables in your test that are available between tests. The other thing is it's lazy, meaning it's not actually assigned until it's called in a test.

let(:radical) { "totally radical" }
# I think it is the same code wise as:
def radical
  return @radical ||= "totally radical"
end

This part of better spec helped me understand it.

@odigity does that help you? There is more going on in RSpec but this is basically how I think about it right now. I'm going to close this because this isn't information we would add to this style guide, but thanks for asking this question! 🤘

@michaeldeol
Copy link

Two things I found online that may help clear it up as well:

subject

Subject blocks allow you to control the initialization of the subject under test. If you don’t have any custom initialization required, then you’re given a default subject method already. All it does is call new on the class you’re testing.

let

Let blocks allow you to provide some input to the subject block that change in various contexts. This way you can simply provide an alternative let block for a given value and not have to duplicate the setup code for the subject over again. Let blocks also work inside of before :each blocks if you need them.

One thing I ran across while researching this question, that we may want to consider adding to this style guide:

"Named Subjects"

subject(:account) { CheckingAccount.new(Money.new(50, :USD)) }

I find that subject can get lost in larger test files, and using a "named subject" may help keep context clear. Thoughts?

Sources:
http://blog.davidchelimsky.net/blog/2012/05/13/spec-smell-explicit-use-of-subject/
http://benscheirman.com/2011/05/dry-up-your-rspec-files-with-subject-let-blocks/

@odigity
Copy link
Author

odigity commented Feb 3, 2015

I see that they're intended to be used for two different (but similar) concepts, but in implementaiton, they seem identical, and therefore, practically interchangeable.

Both created named helper methods that return the result of evaluating a block, which is memoized for each example:

subject(:foo) { MyClass.new } -> expect(foo).to be_whatever
let(:bar) { MyClass.new } -> expect(bar).to be_whatever

And it doesn't matter that one of them was declared with subject and one with let, because you end up with the same result, and use it the same way.

The one exception is with the is_expected.to shortcut, which implictly uses the value of subject(). But once you consider the best practice of only using named subjects, even that diffference disappears.

Hence my confusion.

@michaeldeol
Copy link

@odigity if you look at the example over at the link I posted: http://benscheirman.com/2011/05/dry-up-your-rspec-files-with-subject-let-blocks/

You can use the subject (or named subject) to be the class you are testing against and use the "lets" as a way to change the context in each test, as lets are not actually rendered (created in a sense) until they are called (used).

Example given in the above link (with minor addition):

#improved example
require 'spec_helper'

describe Card do
  subject do
    Card.new(card_type)
  end

  describe "#value" do  
    context "Two of Hearts" do
      let(:card_type) { "2H" }
      its(:value) { should == 2 }
      # or
      expect(subject.value).to eq(2)
    end
  end
end

@odigity
Copy link
Author

odigity commented Feb 3, 2015

But wouldn't the exact opposite work just as well, other than going against the "intention" of the use of the two helpers?

describe Card do
subject(:card_type) { '2H' }
let(:card) { Card.new(card_type) }
if 'is valid with card_type set' do
expect(let).to be_valid
end
end

(I know the language is the opposite of what's intended, but I'm just trying to understand the actual difference in implementation right now... and there doesn't seem to be any...)

@RyanHedges
Copy link
Contributor

let is a method that takes an argument, (:card) in the example. By calling let(:card) you have a method card available for you to use. You do not have a method let available to you. In fact, you'd likely get an error in your test. You could get it to work with let(:let) {...} but it would be weird and hard to read 😛.The purpose of using lets to help isolate behavior.

The thing with subject is this, RSpec knows more about subject. It's special. On of the ways that it's special is found in this implicit example in the docs. You can see it { is_expected.to be_empty } is creating an expectation on subject without you having to write subject anywhere. It just knows that that the object under test is whatever subject is. let does not provide this.

I agree, they have similarities under the hood because it's really just ruby, but their intended use is different.

Just incase someone wants to read the docs in the future:
subject in docs
let in docs

@odigity
Copy link
Author

odigity commented Feb 3, 2015

So far we haven't disagreed, which makes me think maybe I'm failing to get my point across.

let is a method that takes an argument, (:card) in the example.

The same is true of subject().

By calling let(:card) you have a method card available for you to use.

The same is true of subject().

On of the ways that it's special is found in this implicit example in the docs. You can see it { is_expected.to be_empty } is creating an expectation on subject without you having to write subject anywhere.

I specifically pointed that out already as the only thing I know of that makes subject different than let. However, that only works with unnamed subjects, which the RSpec docs specifically recommend against. They recommend named subjects:

subject(:foo) { Foo.new }
if { expect(foo).to be_new }

In which case you could swap out subject for let without changing anything else:

let(:foo) { Foo.new }
if { expect(foo).to be_new }

So, again -- what is the actual difference between the two? Does it just come down to a convention of choosing a primary object (the object being tested), using subject for that, and then using let for any other state you may want to create a helper for?

@russCloak
Copy link
Contributor

russCloak commented Feb 3, 2015

If I could chime in and hopefully say something that hasn't yet been said... the only real difference(s) I know of are:

  1. subject will automatically be assigned as an instance of the class being tested.
  2. It allows for the use of "one liner" tests:
context "when something" do
  subject(:car) { FactoryGirl.create(:car) }

  it { is_expected.to_not be_running }
end

Once again, I hope I contribute to the solution - instead of sparking more conflict.

@odigity
Copy link
Author

odigity commented Feb 4, 2015

I think that perfectly sums it up.

It should additionally be noted that as far as I can tell, you can use the subject in a let block, or a previously defined let in the subject block, since both are only invoked when you invoke them, so there's no real ordering rule, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants