Skip to content

Instantly share code, notes, and snippets.

@puyo
Created November 15, 2013 03:01
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save puyo/7478418 to your computer and use it in GitHub Desktop.
Save puyo/7478418 to your computer and use it in GitHub Desktop.

RSpec 3 syntax is more verbose.

About twice as many characters to type to stub something:

obj.stub(client: client)                          # old
allow(obj).to receive(:client).and_return(client) # new
allow(obj).to receive(client: client)             # possible? still much longer
allow(obj, client: client)                        # I might wrap it in this

More characters to type for every single assertion (multiply by number of assertions desired):

obj.should == value
expect(obj).to eq value          # +4 characters

obj.should_receive(:method)
expect(obj).to receive(:method)  # +4 characters

expect(obj, eq value)            # +2 characters and doesn't read as well
# old frowned upon "its" extension, but look how few keystrokes, and how DRY
its(:body) { should include(text) }

# new (via transpec)
describe '#body' do
  subject { super().body }               # jon would disapprove of this anyway?
  it { is_expected.to include(text) }    # why is there one _ and one . (says my don't make me think brain)
end

its(:body) { is_expected.to include(text) }

I think there are more examples too.

But you know, I don't so much mind the extra typing, it's probably having to type ( that bugs me. That character is significantly harder to type than . and ==.

Was the less metaprogramming, less magical solution worth it?

I have no easy solution to this by the way. Just trying to communicate something I expressed in 140 characters better.

@myronmarston
Copy link

Thanks for writing this up!

To respond to a few specific things you brought up:

allow(obj).to receive(client: client)             # possible? still much longer

This is possible with allow(obj).to receive_messages(client: client). You can pass as many message/return value pairs as you want.

# old frowned upon "its" extension, but look how few keystrokes, and how DRY
its(:body) { should include(text) }

I wouldn't say it's frowned upon. I have specs in some projects that use it. I'd say the abuse of it is frowned upon. I've actually gotten questions like "how do I test methods that take arguments? its doesn't take any arguments"...to which I respond, "you write an example that calls the method with arguments; its was never intended for that". From reading questions on stackoverflow, etc, I get the idea that some users believe the point of RSpec is to write tests in as terse a syntax as possible, and if there's not a one-liner syntax to support what they are trying to test, they feel like that's a deficiency in RSpec that should be addressed. That's not the point of RSpec at all. For an example like yours, I'd probably just test it like this:

it 'includes the text in the body' do
  # I'd probably use a more intention-revealing name than subject, but your example doesn't show that context
  expect(subject.body).to include(text)
end

You say that your its example is so DRY, but there is no duplication of knowledge in my version. It's not any less DRY. Your version is definitely fewer characters, but that has nothing to do with DRY.

it { is_expected.to include(text) }    # why is there one _ and one . (says my don't make me think brain)

It's this way because is_expected is defined simply as expect(subject)...so is_expected.to is the same as expect(subject).to. That said, since it could trip people up, I've been mulling over adding is_expected_to, is_expected_to_not and is_expected_not_to just so whatever users try works. (Kinda like how we support expect().to_not and expect().not_to). Thoughts?

Was the less metaprogramming, less magical solution worth it?

I think so. Have you read my blog post about the new syntax and the reasons for its introduction? I myself experienced odd failures with proxy objects with the should syntax, and it took me like an hour to figure out, even though I was a contributor to RSpec!

The new syntax is also much more consistent (and hopefully, easier to learn) than the old syntax. Consider that RSpec supports 3 kinds of expectations:

  • Value expectations: expecting something about a particular object
  • Block expectations: expecting a result when a block is executed (this is the change, raise_error, throw_symbol and yield matchers -- these are things that aren't static values and require blocks to test).
  • Message expectations: expecting that an object receives a certain message (also called a "mock expectation").

Before introducing the new syntax, these three things had 3 different syntaxes:

x.should matcher
expect { do_something }.to raise_error
x.should_receive(:foo) # not x.should receive(:foo)

These have now been nicely unified:

expect(x).to matcher
expect { do_something }.to raise_error
expect(x).to receive(:foo)

IMO, this consistency is a big win. I also think that this "wrapping" syntax makes it more explicit what RSpec's doing, which is a nice side effect.

But you're right -- these things are more verbose. Tradeoffs, as with all software engineering. I dislike excessive typing as much as anyone, but it's not RSpec's primary design goal to support tests being as terse as possible.

FWIW, we have no current plans to ever drop the old syntax (although, we may move it into an external gem in 4.0 -- that's not decided though), so if you prefer the old syntax and understand the tradeoffs, then keep using the old syntax. That's fine. Transpec even supports options to keep specific parts of the old syntax depending on what you want.

Anyhow, thanks again for engaging, and trying out RSpec 3!

@puyo
Copy link
Author

puyo commented Nov 15, 2013

Firstly, thank you for such a thorough response! I know you're busy. I really appreciate it. Respect+++.

You say that your its example is so DRY, but there is no duplication of knowledge in my version. It's not any less DRY. Your version is definitely fewer characters, but that has nothing to do with DRY.

The lack of DRYness of which I speak is between the specdoc and the test body.

it 'includes the text in the body' do
  # I'd probably use a more intention-revealing name than subject, but your example doesn't show that context
  expect(subject.body).to include(text)
end

The word body is included twice. The concept is included twice (subject.body and 'the body'). Compare it to:

its(:body) { should include(text) }

This example is tiny and contrived but consider a more fully featured example and count the number of times the method under test is repeated either in the test body or in the specdoc. Imagine it is a long method name. I complain in more detail here: https://github.com/rails-oceania/rspec-subject_call/blob/master/README.md#the-situation

The same situation happens with contexts if you use them to describe specific cases rather than generalised assertions.

# specific example
context 'when provided with 23 October 2013' do
  let(:input) { Date.new(2013, 10, 23) }
  it 'should print 2013-10-23'
end

# generalised example
context 'when provided with a date object' do
  let(:input) { Date.new(2013, 10, 23) }
  it 'should print the date in yyyy-mm-dd format'
end

I'm not sure which of these two flavours you think is better. I'm not sure which is better, to be honest. But I have noticed that sometimes my contexts and the first line inside of their blocks sometimes feel very un-DRY. This is not related to RSpec 3 versus RSpec 2 syntax, though.

It's this way because is_expected is defined simply as expect(subject)...

Yes I know. It will probably just take a little getting used to. Are any of these better (or worse? :P) ?

expect_it { to eq 10 }
expect { it.not_to eq 10 }
expect { it.to eq 10 }

The new syntax is also much more consistent.

You're right. And now that I think more about it, this is a tremendous boon to making RSpec easier to learn.

I would like to figure out ways to incrementally get out of the business of writing code for tests and get more into the business of using a Ruby DSL to specify what I expect. In my mind, that means moving away from test names with test bodies that sort of resemble OOP methods and towards more declarative syntax with procedural-based fallback options for tricky tests. I believe this kind of approach would make RSpec much more succinct. I'm yet to figure out what the declarative syntax would actually be, though. ;-)

@myronmarston
Copy link

The lack of DRYness of which I speak is between the specdoc and the test body.

Right. I'm actually very much in favor of using a one-liner when the doc string you would write is identical to the one RSpec can generate for you in the one-liner. IMO, this is what the one-liner feature was intended for and is a great use for it.

However, I personally get a lot of value out of longer, more intention-revealing doc strings. For example, here are some from the code base I work on at my job:

it 'ignores campaign_id by default (since it is used by sharding but not endpoint models)'
it 'logs sauron reload errors to a log file (since they will generally be transient)'
it "retries a few times since we do not want S3 and redis to get out of sync"
it 'can handle there being no title (since title is optional in the packrat schema)'
it 'raises an error if given a namespaced model, since we want to subclass the top-level model'

These longer doc strings help explain the why for a particular behavior, and help guide future decisions when we consider changing a behavior. (We change it, watch a spec break, and read the doc string to remind us the reason why it was there in the first place.)

I'm not sure which of these two flavours you think is better.

Hard to answer that kind of thing without seeing the full example.

Yes I know. It will probably just take a little getting used to. Are any of these better (or worse? :P) ?

IMO they are worse. Long time users to RSpec understand that self in describe is different from self in it -- but new user don't generally know that. The examples you showed there all define an alternate expect and/or an alternate it that does something else from what they normally do. It could work because you've switched the example vs. group context (e.g. you're using expect in the group and it in the example), but I think it'll cause confusion. On top of that, it requires changes both to the example group API (e.g. to add a method like expect or expect_it) and to the example API (e.g. to add to or it). I like the simplicity of is_expected: it is exactly equal to expect(subject) and is very easy to understand. It doesn't require new methods in two contexts. It doesn't use an existing method name for something new.

FWIW, I originally proposed something very similar when I first proposed adding the expect syntax:

rspec/rspec-expectations#119 (comment)

I'm glad we didn't go that route; I find is_expected to be much simpler and less confusing.

You're right. And now that I think more about it, this is a tremendous boon to making RSpec easier to learn.

That's one thing I hope!

I would like to figure out ways to incrementally get out of the business of writing code for tests and get more into the business of using a Ruby DSL to specify what I expect. In my mind, that means moving away from test names with test bodies that sort of resemble OOP methods and towards more declarative syntax with procedural-based fallback options for tricky tests. I believe this kind of approach would make RSpec much more succinct. I'm yet to figure out what the declarative syntax would actually be, though. ;-)

If you want more DSL, give rspec-given a try. It layers some additional DSL constructs on top of DSL and, IMO, is a more well-thought out, consistent approach than rspec-core's one-liners or its.

Personally, I find I prefer the simplicity of starting with the simpler constructs (describe, it) and then, as need arises, using some of the additional constructs like let, before, etc (and, occasionally, a one-liner or its).

@myronmarston
Copy link

BTW, please ping me on twitter when you reply. I don't seem to get email notification for gist comments. (Not sure why).

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