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

Use subject #7

Open
andreareginato opened this Issue Oct 3, 2012 · 28 comments

Comments

Projects
None yet
@andreareginato
Collaborator

andreareginato commented Oct 3, 2012

Write your thoughts about the "use subject" best practice.

@cupakromer

This comment has been minimized.

Show comment
Hide comment
@cupakromer

cupakromer Oct 3, 2012

RSpec 2.11 has the ability to use a named subject. So your last example can be made better with:

subject(:hero) { Hero.first }
it "should be equipped with a sword" do
  hero.equipment.should include "sword"
end

cupakromer commented Oct 3, 2012

RSpec 2.11 has the ability to use a named subject. So your last example can be made better with:

subject(:hero) { Hero.first }
it "should be equipped with a sword" do
  hero.equipment.should include "sword"
end
@myronmarston

This comment has been minimized.

Show comment
Hide comment
@myronmarston

myronmarston Oct 3, 2012

The it { should ... } and its(:blah) { should ... } one-liner syntax have there place, but the documentation formatter output for these is often not what you want. It's OK to use them when the output they produce is exactly what you would have typed for the doc string, but otherwise, I think it's a bad idea to use these.

Note also that its will almost certainly be pulled out of rspec-core and moved into an extension gem in RSpec 3, so I think it's bad to recommend it's use at this point.

myronmarston commented Oct 3, 2012

The it { should ... } and its(:blah) { should ... } one-liner syntax have there place, but the documentation formatter output for these is often not what you want. It's OK to use them when the output they produce is exactly what you would have typed for the doc string, but otherwise, I think it's a bad idea to use these.

Note also that its will almost certainly be pulled out of rspec-core and moved into an extension gem in RSpec 3, so I think it's bad to recommend it's use at this point.

@nclark

This comment has been minimized.

Show comment
Hide comment
@nclark

nclark Oct 3, 2012

+1

i would consider @cupakromer's code a best practice more than

subject { assigns('message') }
it { should match /it was born in Billville/ }
its(:creator) { should match /Topolino/ }

nclark commented Oct 3, 2012

+1

i would consider @cupakromer's code a best practice more than

subject { assigns('message') }
it { should match /it was born in Billville/ }
its(:creator) { should match /Topolino/ }
@cupakromer

This comment has been minimized.

Show comment
Hide comment
@cupakromer

cupakromer commented Oct 3, 2012

You can see David Chelimsky's take on the matter here: http://blog.davidchelimsky.net/2012/05/13/spec-smell-explicit-use-of-subject/

@cupakromer

This comment has been minimized.

Show comment
Hide comment
@cupakromer

cupakromer Oct 16, 2012

Thoughts on using subject like this:

describe CommentsController do
  describe '#create' do
    context 'with valid form' do
      let(:expect_commenting) {
        expect{ post :create, comment: { user_id: 1, body: "This is a comment" } }
      }

      subject {
        post :create, comment: { user_id: 1, body: "This is a comment" }
        @controller
      }

      it { expect_commenting.to change{Comment.count}.by 1 }
      it { should redirect_to comments_path }
      it { should set_the_flash[:notice].to 'Comment Created!' }
    end
  end
end

cupakromer commented Oct 16, 2012

Thoughts on using subject like this:

describe CommentsController do
  describe '#create' do
    context 'with valid form' do
      let(:expect_commenting) {
        expect{ post :create, comment: { user_id: 1, body: "This is a comment" } }
      }

      subject {
        post :create, comment: { user_id: 1, body: "This is a comment" }
        @controller
      }

      it { expect_commenting.to change{Comment.count}.by 1 }
      it { should redirect_to comments_path }
      it { should set_the_flash[:notice].to 'Comment Created!' }
    end
  end
end
@therealadam

This comment has been minimized.

Show comment
Hide comment
@therealadam

therealadam Oct 18, 2012

I think this should follow David Chelimsky's advice and advise using let(:object_under_test) rather than subject. Having heavily used subject.should whatever in the past, I think subject is a little too magical to rely on. That said, I don't use it/should as the example above does.

So, bad:

subject { Thing.new }

it "does things" { subject.should do_things }

Good:

let(:thing) { Thing.new }

it "does things" { thing.should do_things }

therealadam commented Oct 18, 2012

I think this should follow David Chelimsky's advice and advise using let(:object_under_test) rather than subject. Having heavily used subject.should whatever in the past, I think subject is a little too magical to rely on. That said, I don't use it/should as the example above does.

So, bad:

subject { Thing.new }

it "does things" { subject.should do_things }

Good:

let(:thing) { Thing.new }

it "does things" { thing.should do_things }
@cupakromer

This comment has been minimized.

Show comment
Hide comment
@cupakromer

cupakromer Oct 20, 2012

@therealadam in your specific example how does the let(:object_under_test) differ in intent from named subject subject(:object_under_test)? Both are lazy initialized. The named subject version allows the flexibility to choose when to use the name and when it makes sense to be implied.

Granted, you probably shouldn't used a named subject for an arbitrary object that doesn't match the describe constant.

cupakromer commented Oct 20, 2012

@therealadam in your specific example how does the let(:object_under_test) differ in intent from named subject subject(:object_under_test)? Both are lazy initialized. The named subject version allows the flexibility to choose when to use the name and when it makes sense to be implied.

Granted, you probably shouldn't used a named subject for an arbitrary object that doesn't match the describe constant.

@therealadam

This comment has been minimized.

Show comment
Hide comment
@therealadam

therealadam Oct 21, 2012

Mechanically, they are the same. As Chelimsky points out, using object_under_test.should whatever instead of subject.should whatever is more intention-revealing. The only case he recommends implicit subject is to enable the one-liner syntax.

therealadam commented Oct 21, 2012

Mechanically, they are the same. As Chelimsky points out, using object_under_test.should whatever instead of subject.should whatever is more intention-revealing. The only case he recommends implicit subject is to enable the one-liner syntax.

@andreareginato

This comment has been minimized.

Show comment
Hide comment
@andreareginato

andreareginato Mar 9, 2013

Collaborator

Updated the last example and removed the example using its. For the moment I keep suggesting the usage of subject, simply because I've to read the Chelimsky. Anyone can feel free to make a pull request to change the actual definition of the guideline.

Collaborator

andreareginato commented Mar 9, 2013

Updated the last example and removed the example using its. For the moment I keep suggesting the usage of subject, simply because I've to read the Chelimsky. Anyone can feel free to make a pull request to change the actual definition of the guideline.

@zamith

This comment has been minimized.

Show comment
Hide comment
@zamith

zamith Apr 18, 2013

Subject is basically syntactic sugar for a let, therefore I don't believe there's a big difference in using subject(:thing) or let(:thing), the non named subject is the worst, as it is not semantic at all.

That being said I'm not a fan of using let or subject, and the reason is that when you have nested context or describe blocks and lets in each of them, it easily becomes a quest to find where variables are coming from. Causing a mystery guest

I prefer to use plain ruby methods to explicitly refer to the sut (or any other object), or to group a setup block and then use them as instance variables.

I can live with using let if only in the first level of nesting, and for the one-liner syntax (even though I'm personally not a big fan).

Does anyone agree with this?

zamith commented Apr 18, 2013

Subject is basically syntactic sugar for a let, therefore I don't believe there's a big difference in using subject(:thing) or let(:thing), the non named subject is the worst, as it is not semantic at all.

That being said I'm not a fan of using let or subject, and the reason is that when you have nested context or describe blocks and lets in each of them, it easily becomes a quest to find where variables are coming from. Causing a mystery guest

I prefer to use plain ruby methods to explicitly refer to the sut (or any other object), or to group a setup block and then use them as instance variables.

I can live with using let if only in the first level of nesting, and for the one-liner syntax (even though I'm personally not a big fan).

Does anyone agree with this?

@nolastan

This comment has been minimized.

Show comment
Hide comment
@nolastan

nolastan Apr 26, 2013

Doesn't the example go against Don't use should?

nolastan commented Apr 26, 2013

Doesn't the example go against Don't use should?

@cupakromer

This comment has been minimized.

Show comment
Hide comment
@cupakromer

cupakromer Apr 28, 2013

Most of this is personal / project preferences. So there is no one "proper" way to handle things. If things are working for you, then keep going.

@zamith can you provide an example on how you prefer ruby methods? They suffer from the same sort of "mystery guest" problem when deep nesting is involved. So I'm not sure I see what you are gaining with them.

However, I have seen some crazy test files using let and subjects for everything. Though this usually was because the testing was started by defining everything via let before use (premature extraction). I much prefer writing tests long form (all code / vars in the it block) then doing a refactor step extracting common shared code.

Here's my personal preferences:

  • Use let for memoization of variables which are shared across tests
  • Use subject (named) for the object under test (this provides a semantic clue)
  • Use methods for helper actions and consistent generation of intermediate objects
  • Define other variables that do not meet the above criteria in the test itself as locals
  • Prefer let and subject defined variables to instance variables (barewords vs @vars)

cupakromer commented Apr 28, 2013

Most of this is personal / project preferences. So there is no one "proper" way to handle things. If things are working for you, then keep going.

@zamith can you provide an example on how you prefer ruby methods? They suffer from the same sort of "mystery guest" problem when deep nesting is involved. So I'm not sure I see what you are gaining with them.

However, I have seen some crazy test files using let and subjects for everything. Though this usually was because the testing was started by defining everything via let before use (premature extraction). I much prefer writing tests long form (all code / vars in the it block) then doing a refactor step extracting common shared code.

Here's my personal preferences:

  • Use let for memoization of variables which are shared across tests
  • Use subject (named) for the object under test (this provides a semantic clue)
  • Use methods for helper actions and consistent generation of intermediate objects
  • Define other variables that do not meet the above criteria in the test itself as locals
  • Prefer let and subject defined variables to instance variables (barewords vs @vars)
@cupakromer

This comment has been minimized.

Show comment
Hide comment
@cupakromer

cupakromer Apr 28, 2013

@nolastan you are correct. Fixed in #57.

cupakromer commented Apr 28, 2013

@nolastan you are correct. Fixed in #57.

@zamith

This comment has been minimized.

Show comment
Hide comment
@zamith

zamith Apr 28, 2013

@cupakromer The ruby methods will be on the outer namespace, so they are global to that file. Also, they must be explicitly called, so there's no mystery guest.

describe "something" do
  before :each do
    @var = 10
  end
  context "inner 1" do
    before :each do
      @var = 5
    end
    it "does something" do
      @var.should eq 5
    end
  end
  context "inner 2" do
    it "does something" do
      @var.should eq 10
    end
  end
end

as opposed to

describe "something" do  
  context "inner 1" do
    it "does something" do      
      var_5.should eq 5
    end
  end
  context "inner 2" do
    it "does something" do
      var.should eq 10
    end
  end
  def var
    10
  end
  def var_5
    5
  end
end

This is a silly example, but it shows that in the first example @var takes different values and is context specific as opposed to the second example, where the var and var_5 are explicitly called and never vary. This being said on regular use cases I would prefer something like:

describe "something" do  
  context "inner 1" do
    it "does something" do
      prepare_variable
      @var.should eq 5
    end
  end
  def prepare_variable
    5
  end
end

which is much more explicit in terms of having a setup phase and thus following the four phase test pattern

zamith commented Apr 28, 2013

@cupakromer The ruby methods will be on the outer namespace, so they are global to that file. Also, they must be explicitly called, so there's no mystery guest.

describe "something" do
  before :each do
    @var = 10
  end
  context "inner 1" do
    before :each do
      @var = 5
    end
    it "does something" do
      @var.should eq 5
    end
  end
  context "inner 2" do
    it "does something" do
      @var.should eq 10
    end
  end
end

as opposed to

describe "something" do  
  context "inner 1" do
    it "does something" do      
      var_5.should eq 5
    end
  end
  context "inner 2" do
    it "does something" do
      var.should eq 10
    end
  end
  def var
    10
  end
  def var_5
    5
  end
end

This is a silly example, but it shows that in the first example @var takes different values and is context specific as opposed to the second example, where the var and var_5 are explicitly called and never vary. This being said on regular use cases I would prefer something like:

describe "something" do  
  context "inner 1" do
    it "does something" do
      prepare_variable
      @var.should eq 5
    end
  end
  def prepare_variable
    5
  end
end

which is much more explicit in terms of having a setup phase and thus following the four phase test pattern

@zamith

This comment has been minimized.

Show comment
Hide comment
@zamith

zamith Apr 28, 2013

@cupakromer I agree with most of what you're saying. lets can be really helpful, but if they are not used with care and if they are used across several levels of nesting you're in all kinds of mess.

zamith commented Apr 28, 2013

@cupakromer I agree with most of what you're saying. lets can be really helpful, but if they are not used with care and if they are used across several levels of nesting you're in all kinds of mess.

@cupakromer

This comment has been minimized.

Show comment
Hide comment
@cupakromer

cupakromer Apr 29, 2013

@zamith agree with needing to be careful. However, you have the same problem with methods. If you used them across several levels of nesting, you have the same problem. Though this is getting a bit far from the topic of subject here. I'd be happy to discuss further on irc #rspec or in the let issue #8.

cupakromer commented Apr 29, 2013

@zamith agree with needing to be careful. However, you have the same problem with methods. If you used them across several levels of nesting, you have the same problem. Though this is getting a bit far from the topic of subject here. I'd be happy to discuss further on irc #rspec or in the let issue #8.

@celesteking

This comment has been minimized.

Show comment
Hide comment
@celesteking

celesteking Jun 2, 2014

that's bullshit. if you write 1 section above that you must use "expect" syntax, then be sure to use that very syntax in this section.

Otherwise people think that "expect" is bad because you make it red boxed. You transition from expect to should, which must not happen.

celesteking commented Jun 2, 2014

that's bullshit. if you write 1 section above that you must use "expect" syntax, then be sure to use that very syntax in this section.

Otherwise people think that "expect" is bad because you make it red boxed. You transition from expect to should, which must not happen.

@andreareginato

This comment has been minimized.

Show comment
Hide comment
@andreareginato

andreareginato Jun 3, 2014

Collaborator

If you feel like there are some inconcistencies, please, make a pull
request.
We are more than happy to improve betterspecs.

On Mon, Jun 2, 2014 at 5:54 PM, celesteking notifications@github.com
wrote:

that's bullshit. if you write 1 section above that you must use "expect"
syntax, then be sure to use that very syntax in this section.

Otherwise people think that "expect" is bad because you make it red boxed.
You transition from expect to should, which must not happen.


Reply to this email directly or view it on GitHub
#7 (comment)
.

Andrea Reginato
Lelylan | reThink your house
http://lelylan.com

Collaborator

andreareginato commented Jun 3, 2014

If you feel like there are some inconcistencies, please, make a pull
request.
We are more than happy to improve betterspecs.

On Mon, Jun 2, 2014 at 5:54 PM, celesteking notifications@github.com
wrote:

that's bullshit. if you write 1 section above that you must use "expect"
syntax, then be sure to use that very syntax in this section.

Otherwise people think that "expect" is bad because you make it red boxed.
You transition from expect to should, which must not happen.


Reply to this email directly or view it on GitHub
#7 (comment)
.

Andrea Reginato
Lelylan | reThink your house
http://lelylan.com

@dideler

This comment has been minimized.

Show comment
Hide comment
@dideler

dideler Jun 13, 2014

The BAD example is

it { expect(assigns('message')).to match /it was born in Belville/ }
it { expect(assigns('message').creator).to match /Topolino/ }

while the GOOD example is

subject { assigns('message') }
it { should match /it was born in Billville/ }

The GOOD example is missing the second test of the creator.

dideler commented Jun 13, 2014

The BAD example is

it { expect(assigns('message')).to match /it was born in Belville/ }
it { expect(assigns('message').creator).to match /Topolino/ }

while the GOOD example is

subject { assigns('message') }
it { should match /it was born in Billville/ }

The GOOD example is missing the second test of the creator.

@diegoguemes

This comment has been minimized.

Show comment
Hide comment
@diegoguemes

diegoguemes Oct 23, 2015

I just visited this thread to check if more people considered the use of the word subject an aberration inside the body of an spec. I'm so glad more people think in the same way! 😄

Just for clarifying, I'm totally ok with the subject syntax, I disagree with the use of subject without a named variable, because it doesn't express any intent.

The post http://blog.davidchelimsky.net/blog/2012/05/13/spec-smell-explicit-use-of-subject/, which some people suggested, is a great read

diegoguemes commented Oct 23, 2015

I just visited this thread to check if more people considered the use of the word subject an aberration inside the body of an spec. I'm so glad more people think in the same way! 😄

Just for clarifying, I'm totally ok with the subject syntax, I disagree with the use of subject without a named variable, because it doesn't express any intent.

The post http://blog.davidchelimsky.net/blog/2012/05/13/spec-smell-explicit-use-of-subject/, which some people suggested, is a great read

@andreareginato

This comment has been minimized.

Show comment
Hide comment
@andreareginato

andreareginato Oct 23, 2015

Collaborator
Collaborator

andreareginato commented Oct 23, 2015

@onebree

This comment has been minimized.

Show comment
Hide comment
@onebree

onebree Oct 23, 2015

Contributor

@diegoguemes yes, please submit a pull request, including that blog link in the comments!

Contributor

onebree commented Oct 23, 2015

@diegoguemes yes, please submit a pull request, including that blog link in the comments!

@maximveksler

This comment has been minimized.

Show comment
Hide comment
@maximveksler

maximveksler Oct 18, 2016

It seems that RSpec official docs recommend subject not be used in user facing examples.

While the examples below demonstrate how subject can be used as a
user-facing concept, we recommend that you reserve it for support of custom
matchers and/or extension libraries that hide its use from examples.

https://www.relishapp.com/rspec/rspec-core/v/3-5/docs/subject/implicitly-defined-subject

Am I getting this wrong ?

maximveksler commented Oct 18, 2016

It seems that RSpec official docs recommend subject not be used in user facing examples.

While the examples below demonstrate how subject can be used as a
user-facing concept, we recommend that you reserve it for support of custom
matchers and/or extension libraries that hide its use from examples.

https://www.relishapp.com/rspec/rspec-core/v/3-5/docs/subject/implicitly-defined-subject

Am I getting this wrong ?

@dideler

This comment has been minimized.

Show comment
Hide comment
@dideler

dideler Oct 20, 2016

@maximveksler note that the page is about the implicit subject. I wonder what their standpoint is on the explicit (i.e. named) subject.

dideler commented Oct 20, 2016

@maximveksler note that the page is about the implicit subject. I wonder what their standpoint is on the explicit (i.e. named) subject.

@Schniz

This comment has been minimized.

Show comment
Hide comment
@Schniz

Schniz Sep 27, 2017

How do you test a "service"/lib class?

Let's say we have a class:

class DomainSanitizer
  def sanitize(url)
    # ...
  end
end

DomainSanitizer.sanitize("https://google.com") # => "google.com"
DomainSanitizer.sanitize("google.com/some_path#with_hash") # => "google.com"

how would you rather test it:

Without subjects

describe DomainSanitizer do
  let(:domain) { "google.com" }
  it "cleans urls with scheme" { expect(described_class.sanitize("https://#{domain}")).to eq "google.com" }
  it "cleans urls with path" { expect(described_class.sanitize("#{domain}/some_path")).to eq "google.com" }
end

With subject

describe DomainSanitizer do
  let(:domain) { "google.com" }
  context "with scheme" do
    subject { described_class.sanitize("https://#{domain}") }
    it { is_expected.to eq domain }
  end
  # ... more tests like this ...
end

Construct it using code?

describe DomainSanitizer do
  let(:domain) { "google.com" }
  {"scheme" => "https://#{domain}",
   "path and hash" => #{domain}/path#hash"}.each do |ctx, url|
    # it?
    # it "takes domain from url with #{ctx}" { expect(described_class.sanitize(url)).to eq domain }

    # subject?
    context ctx do
      subject { described_class.sanitize(url) }
      it { is_expected.to eq domain }
    end
  end
end

cc/ @wildcard

Schniz commented Sep 27, 2017

How do you test a "service"/lib class?

Let's say we have a class:

class DomainSanitizer
  def sanitize(url)
    # ...
  end
end

DomainSanitizer.sanitize("https://google.com") # => "google.com"
DomainSanitizer.sanitize("google.com/some_path#with_hash") # => "google.com"

how would you rather test it:

Without subjects

describe DomainSanitizer do
  let(:domain) { "google.com" }
  it "cleans urls with scheme" { expect(described_class.sanitize("https://#{domain}")).to eq "google.com" }
  it "cleans urls with path" { expect(described_class.sanitize("#{domain}/some_path")).to eq "google.com" }
end

With subject

describe DomainSanitizer do
  let(:domain) { "google.com" }
  context "with scheme" do
    subject { described_class.sanitize("https://#{domain}") }
    it { is_expected.to eq domain }
  end
  # ... more tests like this ...
end

Construct it using code?

describe DomainSanitizer do
  let(:domain) { "google.com" }
  {"scheme" => "https://#{domain}",
   "path and hash" => #{domain}/path#hash"}.each do |ctx, url|
    # it?
    # it "takes domain from url with #{ctx}" { expect(described_class.sanitize(url)).to eq domain }

    # subject?
    context ctx do
      subject { described_class.sanitize(url) }
      it { is_expected.to eq domain }
    end
  end
end

cc/ @wildcard

@Schniz

This comment has been minimized.

Show comment
Hide comment
@Schniz

Schniz Sep 27, 2017

a truth table is an option as well, to construct tests through code:

def url(scheme, domain, path, hash)
  result = ""
  result += "#{scheme}://" unless scheme.nil?
  result += "#{domain}" unless domain.nil?
  result += "/#{path}" unless path.nil?
  result += "##{hash}" unless hash.nil?
  result
end

[
	url("https", "google.com", "path", "hash"),
	url(nil    , "google.com", "path", "hash"),
	url("https", "google.com", nil   , "hash"),
	url("https", "google.com", "path", nil   ),
	# .... etcccc ....
].each do |url|
  # ...
end

Schniz commented Sep 27, 2017

a truth table is an option as well, to construct tests through code:

def url(scheme, domain, path, hash)
  result = ""
  result += "#{scheme}://" unless scheme.nil?
  result += "#{domain}" unless domain.nil?
  result += "/#{path}" unless path.nil?
  result += "##{hash}" unless hash.nil?
  result
end

[
	url("https", "google.com", "path", "hash"),
	url(nil    , "google.com", "path", "hash"),
	url("https", "google.com", nil   , "hash"),
	url("https", "google.com", "path", nil   ),
	# .... etcccc ....
].each do |url|
  # ...
end
@mvz

This comment has been minimized.

Show comment
Hide comment
@mvz

mvz Nov 23, 2017

@Schniz Definitely the version under "Without subjects", but with multi-line do blocks.

mvz commented Nov 23, 2017

@Schniz Definitely the version under "Without subjects", but with multi-line do blocks.

@baash05

This comment has been minimized.

Show comment
Hide comment
@baash05

baash05 Jan 15, 2018

I have three tests

expect(subject).to receive(:name)
expect(task).to receive(:name)
expect(user).to receive(:name)

This is the first time I've ever seen this test file and I'm reading these tests for the first time
I've got the context defined 50 lines above, and this test is failing, what is "subject"
I've got the context defined 50 lines above, and this test is failing, what is "user"

With the variable name "user" I don't have to guess or search or anything. It's very likely a user

baash05 commented Jan 15, 2018

I have three tests

expect(subject).to receive(:name)
expect(task).to receive(:name)
expect(user).to receive(:name)

This is the first time I've ever seen this test file and I'm reading these tests for the first time
I've got the context defined 50 lines above, and this test is failing, what is "subject"
I've got the context defined 50 lines above, and this test is failing, what is "user"

With the variable name "user" I don't have to guess or search or anything. It's very likely a user

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment