Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Add support for `expect(value)` syntax. #119

Merged
merged 9 commits into from

12 participants

Myron Marston Justin Ko David Chelimsky Tim Pope Zach Dennis Alex Rothenberg Sam Goldman Pat Maddox Tom Stuart Don't Add Me To Your Organization a.k.a The Travis Bot Coveralls Jon Rowe
Myron Marston
Owner

Note: I'm opening this pull request just to start a discussion about this. It's not ready to be merged yet.

Feature Summary

This adds an alternate syntax to the existing should syntax for setting expectations. It's based on the already existing use of expect for block expectations but makes it work for normal ones, too:

expect(something).to be_awesome
# rather than:
something.should be_awesome

I've already had a few conversions with @dchelimsky and @justinko about this. There are some details we need to work out and I also want to get feedback from a wider base of RSpec users.

Here's a summary of why I think this new syntax has value:

  1. Currently, there are two syntaxes for setting expectations: should for normal expectations, and expect for block expectations (or you can fall back to using a lambda/proc). This unifies them: you can use expect for both.
  2. This also aligns the syntax with Jasmine, which can be nice for people working on projects that use both.
  3. I believe that all matchers will work just fine with this new syntax.
  4. This opens up the possibility of not monkey patching every object in the system with should and should_not.
  5. should and should_not are prone to problems related to the fact that any object can undefine or redefine them on itself and it suddenly can break rspec-expectations. We've actually seen a few cases of this (where it was completely unintentional, in fact!). Consider the case of a proxy object that uses BasicObject, defines a method (proxy_method) and proxies the rest to a target object using method_missing. An expectation like proxy.should respond_to(:proxy_method) can wrongly fail, because should will be proxied through to the target object, so this winds up being target.should respond_to(:proxy_method). In contrast, expect(proxy).to respond_to(:proxy_method) would work just fine. For some more cases of problems like these, see #114, rspec/rspec-core#471 and rspec/rspec-rails#445.

The last one is the biggie for me. I've been bitten by weird, hard-to-track-down bugs with should on delegate objects.

Open Questions

So, open questions for discussion:

  1. Is anyone against adding this as an alternate syntax to rspec-expecations?
  2. Does it makes sense to provide a way for someone to use rspec-expectations w/o should and should_not being monkey-patched onto Kernel? The value I see here is that if people decide that expect is the preferred syntax for a given project, it would be nice to be able to help enforce uniformity by preventing should and should_not from being used (which, in turn, would ensure the project never has any of the weird proxy-object should issues we have seen). Note that I wouldn't consider every removing should and should_not from rspec-expectations entirely. There's too much code out in the wild that uses and it generally works fine.
  3. If we do want to provide a way to disable should/should_not, what should that mechanism be?
  4. If we decide to provide the means, and decide that expect is the preferred syntax, would it make sense to disable should and should_not by default in some future major release (i.e. make it opt-in for that syntax, rather than opt-out).

My Two Cents

I like this syntax a lot (obviously; that's why I opened this PR!), and I'd like a way to be able to disable should and should_not on future projects. I'm not yet sure what that way should be, but one possibility is the existing expect_with option in rspec-core. Maybe it could support expect_with :rspec for all of rspec-expectations, and expect_with :rspec_only_expect (or something better named) for rspec-expectations w/o should. As for #4: if rspec users like the expect syntax, and we can ensure a smooth transition, and provide could deprecation messages, I'd probably be in favor of disabling should by default in 3.0 or 4.0, simply because it avoids a whole class of issues.

If you're an RSpec user and you have any opinion on this whatsoever, please leave a comment!

Justin Ko

I'd love to see this in 2.9, and make should opt-in in 3.0.

Regarding excluding should, I would rather see it more explicit than another expect_with option. Something like RSpec::Expectations.include_should = false.

Good stuff!

David Chelimsky
Owner
  1. Not yet :)
  2. Yes
  3. config.enable_should_and_should_not = false
  4. Yes
David Chelimsky
Owner

@justinko 2.9 is imminent and this is going to take some time, so probably 2.10 or 3.0 (if there isn't a 2.10). If it's 3.0, I think we're safer leaving it an opt-in and make it opt-out in 4.0).

David Chelimsky
Owner

@myronmarston what about the it { should matcher } syntax?

spec/rspec/expectations/expectation_target_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+module RSpec
+ module Expectations
+ describe ExpecationTarget do
+ context 'when constructed via #expect' do
+ it 'constructs a new instance targetting the given argument' do
+ expect(7).target.should eq(7)
David Chelimsky Owner

This is funny - we're using the old syntax to specify the new. Reminds me of rspec's early days when rspec was tested w/ test/unit.

Do you think expect(expect(7).target).to eq(7) would be too much of a leap at this point?

Myron Marston Owner

For all these examples, I started with just the new syntax and empty to and not_to methods, but everything (wrongly) passed since to and not_to were no-ops. Using should here got me correctly passing and failing examples, and gave me confidence I was actually specifying something :).

We can certainly refactor in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/rspec/expectations/expectation_target.rb
@@ -0,0 +1,29 @@
+module RSpec
+ module Expectations
+ class ExpecationTarget
+ attr_reader :target
David Chelimsky Owner

Any reason, besides our own internal specs, to expose this? If not, I'd prefer to hide it to reduce surface area. I'm also not sold on the ExpectationTarget name, and this sorta locks it in.

Myron Marston Owner

The initial reason I added it is the internal specs. It was handy to be able to specify what the target was for different cases.

Once I added it, though, it struck me that it could be a useful public API for people that want to add extensions to this--but that's obviously premature. If you're prefer, I can move the definition of the attr_reader into the spec file so that it's available for our specs but isn't an API available to others.

BTW, what don't you like about ExpectationTarget? Often times I write code and I'm not happy with the names, but in this case, I thought it was the perfect name for this.

David Chelimsky Owner

Definitely prefer not to expose a public API for target at this stage.

re: ExpectationTarget, I don't dislike it, I just don't like it (there's a difference). Mainly because it's a new concept/word. We already use "actual" in matchers, so one alternative would be Actual. I don't feel strongly about this at this point because it's internal (and we can change it), but I will feel strongly about it if the name becomes part of the public API.

Myron Marston Owner

I thought about the fact we call things actual, but I couldn't come up with a good class name that incorporated it. Just calling the class Actual seems funny (and, in isolation like that, not particularly descriptive). ExpectationActual sounds funny to me, too. Also, is there any public API that uses the term actual, or is that just the typical variable name chosen internally by matchers?

David Chelimsky Owner

I've used target before and was sometimes confused about what it meant (actual or expected).

Let's leave it as ExpectationTarget for now. If a better name emerges we can change it as long as we don't expose APIs around the name.

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

so probably 2.10 or 3.0

@dchelimsky for some reason my brain couldn't process the fact that we can increment from 2.9 to 2.10 rather than 2.9 to 3.0 :sleepy:

config.enable_should_and_should_not = false

IMO, It feels strange to me to have a config option for a library that is opt-in. Oh well.

Justin Ko

IMO, It feels strange to me to have a config option for a library that is opt-in. Oh well.

@dchelimsky maybe config.expectation_framework.enable_should_and_should_not = false

That way we don't bind RSpec Core to opt-in libraries.

UPDATE: Or rather I should say, let opt-in libs leak into RSpec Core. Right now it's all contained in config.expect_with.

David Chelimsky
Owner

@justinko it would be defined in rspec-expectations via config.add_setting, so no binding.

What you propose, however, might be a cleaner way to handle config settings for extensions. It would certainly clarify where the options live, and it might allow us to clean up Configuration a bit.

Justin Ko

it would be defined in rspec-expectations via config.add_setting, so no binding.

@dchelimsky RSpec Expectations can be run stand-alone (e.g. w/ T/U), so that would be binding it to RSpec Core :smile:

Justin Ko

@dchelimsky we do a if RSpec.respond_to?(:configure) in expectations, so nevermind.

Tim Pope

Ever since we started turning our nose up at the noise word "should" in example descriptions, it's kind of irked me we still use it in examples themselves. So, from that perspective alone, I like this.

David Chelimsky
Owner

RSpec.configuration.add_setting(:enable_should_and_should_not) if defined?(RSpec.configuration)

David Chelimsky
Owner

@justinko That said, I really like the idea of a name for the extension.

Justin Ko

@dchelimsky and in the case of multiple expectation/mock frameworks:

config.expectation_framework(:rspec).enable_should_and_should_not = true
config.expectation_framework(:test_unit)
config.mock_framework(:rr)

Just setup a mapping from symbol to module/constant. Obviously, not passing a symbol would return the first framework.

Sorry to take this off track folks!

Justin Ko

@tpope EXACTLY.

Which brings up an interesting point: maybe should_receive could be receives...

David Chelimsky
Owner

@justinko expects (like mocha).

Myron Marston
Owner

@myronmarston what about the it { should matcher } syntax?

Great question. I didn't even consider that. I don't use it very often. You could still use specify { expect(whatever).to matcher } for one liners (although, it's not the same, with the implicit subject and what not). If anyone has a better idea we can consider it, but I'm ok with the disabling of should and should_not being a trade off that means you no longer have that syntax available.

This actually brings up a good point in favor of the config option being exposed by rspec-core (but added by expectations, as you've suggested): it means that rspec-core will have access to it and can thereby choose not add these methods.

One reason I opened this PR now was to hopefully get this new syntax in a 2.x release (potentially with should/should_not being opt-out) so that we have the possibility of making it opt-in for 3.0. That said, if 3.0's just around the corner that might be moving too quickly. If we do want to do that, or something like it, I had an idea for a way to ease the transition. Given that people will update to 3.0 w/o reading the fine print to understand what they have to configure to keep should and should_not available, it could be good to do something like:

class Object
  def method_missing(name, *args, &block)
    if %w[ should should_not ].include?(name.to_s)
      warn "should and should_not are not available by default in RSpec 3.0. To enable them, do XYZ."
    end
    super
  end
end

Redefining Object#method_missing like this kinda scares me but I can't think of a better way to give people a friendly heads-up about the change.

David Chelimsky
Owner

Whether we release the configuration option as 2.x or 3.0, I want the default to start off the way it is now (with should and should_not available). If that means leaving it as the default until 4.0, so be it. That message will frustrate people, so I'd rather wait until expect has lived for a while as part of the lib.

The should used for one-liners is defined on ExampleGroup (not globally on object). I think it would be OK to leave that definition there, but have it delegate to expect instead of the global should.

Myron Marston
Owner

Actually, here's a pretty decent syntax for one-liners with expect:

describe MyModel do
  expect_it { to validate_presence_of(:name) }
end

I'm generally reluctant to add new methods to the example group DSL, but here it's just an alias for it that reads nicer. to, not_to and to_not can be defined on Example to delegate to expect(subject).to/to_not.

@dchelimsky -- you're right that it does not harm to leave the should and should_not one-liner syntax since it's not the global should on kernel, but I think it would be jarring to disable should and should_not for a project and then still see it used liike that.

Myron Marston
Owner

Random thought that occurs to me about my suggested expect_it syntax above: to me, expect_it suggests what's really happening (i.e. expect(subject)) in a way that the old syntax didn't. That's a plus!

David Chelimsky
Owner

I'd rather figure out a way to configure things such that should remains should for one liners. Some possibilities:

config.enable_should_for_one_liners = true
config.enable_should_on_every_object = false

# or

config.enable_should :scope => :global
config.enable_should :scope => :one_liners

#etc
Zach Dennis

I vote for having separate methods for this configuration opposed to the latter. The latter option looks like the second call to #enable_should is overriding the first.

Myron Marston
Owner

I'd rather figure out a way to configure things such that should remains should for one liners.

I'm not sure I agree, but I don't necessarily disagree. Why would you prefer to keep should for one liners?

David Chelimsky
Owner

There's a difference between reducing risks associated with a method added to all objects and should. I have no issue w/ should (re: @tpope's comment above, the move away from "should" in messages is orthogonal to any move away from "should" for expectations), and whatever we do here I'd like to minimize the friction imposed by such a change.

For me, expect_it { to matcher } has no real advantage to it { should matcher } and I think it has a couple of disadvantages: it's new syntax, and it's subtly different syntax from the in-line syntax. We'd end up fielding countless "expect { to matcher } gives a NoMethodError" bug reports.

Alex Rothenberg

If its just a question of syntax for the one liners what about expects_to like this?

describe MyModel do
  it { expects_to validate_presence_of(:name) }
end

Oh, and I do like the whole idea of this change. +1

Sam Goldman

What about 3rd party libraries that extend RSpec? I maintain such a library, which uses "should," "should_not," and "should_receive" internally. I think that if these methods are removed in a future version, I would like to still let my library work with pre-removal and post-removal versions, instead of having to maintain two branches of my code. Can this change be implemented such that libraries continue to work?

David Chelimsky
Owner

@samwgoldman we're talking about opt-in/out, not removal, so there shouldn't be a conflict.

Sam Goldman

@dchelimsky Okay, but just to be absolutely certain I understand: if the application owner opts out, would an extension like this still work? https://gist.github.com/1777846

Myron Marston
Owner

@samwgoldman -- two suggestions:

  1. Once we add the expect syntax, I think it'll always be available (especially since it's already available for block expectations). So, you could refactor that to use the expect syntax and it would regardless of whether or not the user has opted in/out of should/`should_not. Note that if you did that your extension would not work on old rspec versions before this change.
  2. Your extension could opt-in to the should/should_not syntax if the config option is available. That would allow it to work on old and new versions of RSpec. It would force the user to opt-in to the syntax, though.
Myron Marston
Owner

@dchelimsky -- what do you think about getting this in the next 2.x release? I think it adds value even if there's no opt-in/opt-out option yet. Are you satisfied with the implementation? I still need to document this, obviously.

As far as the opt-out implementation goes, I was thinking of something like this:

module Kernel
  undef should
  undef should_not
end

We need to work out how to trigger that, but I think that should work sufficiently.

We've talked about a configuration API that's available from RSpec.configure, but I think we should also provide a documented way to opt-out of should/should_not when rspec-expectations is used w/o the rest of RSpec. Should there be a config option for that? Or just a file the user needs to require (i.e. require 'rspec/expectations/no_should')?

David Chelimsky
Owner

@myronmarston this is a big change and I still have a bunch of questions and zero time to ask them. This is going to take a while.

David Chelimsky
Owner

My original thought was expect(actual, matcher=nil, message=nil), with some of the matchers rewritten from a different perspective:

expect(actual.predicate?)
expect(actual, eq(3))
expect(actual, gte(3), "there should be at least 3 of these ....")

The syntax is simple and consistent (it's just a function), and the matchers can all be reused as mock argument matchers: obj.should_receive(:msg).with(gte(3)). This feels more like an internal DSL to me, which I like. When RSpec was originally conceived it was trying to bridge the gap between internal and external DSL because there was no story runner concept. These days we have Gherkin tools like Cucumber and Turnip, so RSpec doesn't need to bridge that gap any longer: an internal DSL is sufficient - arguably better.

The downside is that it requires an alternate matcher library.

The really nice thing about your suggestion is matchers stay the same - we're just adding new syntax. The fact that the new syntax doesn't buy us the alignment with rspec-mocks, however, significantly reduces the value proposition.

I'll try to follow up this weekend with a more concrete list of what I view as requirements for this change to happen.

Myron Marston

I like the simplicity of what you've suggested above. There's less conceptual overhead then with the existing syntax. It totally goes beyond what I was trying to do here, though, and as you point out--what you've suggested is essentially a whole-sale rewrite of rspec-expectations (or maybe a new library that's an alternative to rspec-expectations?). While I'd probably prefer the syntax you've suggested if rspec-expectations didn't exist and we were building it from scratch and deciding what syntax to use...given that rspec-expectations does exist, the improvement it provides over expect(...).to isn't such that I would think that it's worth the effort to do a rewrite or new library, with all that entails (including dealing with bugs that have already been dealt with here).

A couple questions from your proposed syntax:

  • How are block expectations dealt with? Do users have to use proc or lambda and pass it as the first argument? One of the things I really like about expect(...).to is the unified syntax it provides for expectations on objects and expectations on blocks. I'm not sure how you'd achieve that with the syntax you've proposed.
  • It's unclear to me how expect(actual.predicate?) is intended to work. Does that imply that if a truthy value is passed as the first argument, the expectation passes? That seems potentially error prone since pretty much every object in ruby is truthy, so if someone write expect(actual) (and forgot to pass a matcher), then it would pass. Also, while the failure could be something like "expected a truthy value, got false", that output isn't as helpful as the output we get now from actual.should be_predicate or expect(actual).to be_predicate: "expected predicate? to return true, got false".
  • You mention that your proposal syntax buys us alignment with rspec-mocks, but I don't follow that point. The example you show (obj.should_receive(:msg).with(gte(3))) has rspec-mocks still using a should-based syntax. Also, you mention the benefit of matchers working for rspec-mocks as argument matchers...but that already works, right? Here's an example that works as expected:
RSpec::Matchers.define :multiple_of do |expected|
  match do |actual|
    actual % expected == 0
  end
end

describe "Using matchers for mock argument matchers" do
  it 'passes a passing arg matcher' do
    k = mock
    k.should_receive(:foo).with(multiple_of(3))
    k.foo(9)
  end

  it 'fails a failing arg matcher' do
    k = mock
    k.should_receive(:foo).with(multiple_of(3))
    k.foo(8) # this fails the example
  end
end
David Chelimsky
Owner

You mention that your proposal syntax buys us alignment with rspec-mocks, but I don't follow that point.

What I meant was that the "voice" of the matchers works equally well for expectations and argument expectations:

expect(result, hash_including(:a => :b))
foo.should_receive(:bar).with(hash_including(:a => :b))

Compare that to:

result.should be_hash_including(:a => :b))
foo.should_receive(:bar).with(hash_including(:a => :b))
David Chelimsky
Owner

One inspiration for this is Jay Fields' expectations
framework
. The
syntax was like this:

expect expected do
  # do stuff that returns actual
end

In expectations, that is a complete example. There are no docstrings because
Jay thinks they are just noise, and without them you are encouraged to make
your code more self-explanatory. This is dead simple for value/state testing,
and also comes with a simple DSL for mocking/stubbing:

expect mock.to.receive(:log) do |logger|
  service = Service.new(:logger => logger)
  service.do_something_that_logs_a_message
end

As flexible as this is, it is very constraining (one expectation per example,
for example). which is Jay's whole point. In rspec-expectations, we could use
a similar syntax:

it "raises an exception" do
  thing = Thing.new
  expect Exception, "that was exceptional" do
    thing.something_exceptional
  end
end

... and support matchers as arguments to expect:

example do
  estimator = Estimator.new
  expect gte(5) do
    estimator.estimate
  end
end

With a block but no other args, it would work as it already does:

example do
  expect { post :create, :thing => {} }.to change(Thing, :count)
end

And we'd also be able to support a 2-arg/no block syntax:

expect 3, calculator.add(1,2)

Although perhaps it would be better to force that to be:

expect 3 do
  calculator.add(1,2)
end

... for consistency.

Thoughts?

Justin Ko

I actually like the readability and verboseness of the current syntax:

expect(5).to eq(5)
# vs
expect(5) { 5 }

expect { calculator.add(1, 2) }.to eq(3)
# vs
expect(3) { calculator.add(1, 2) }

I don't know, I like them both. But I do agree with @myronmarston in that Jay's syntax isn't a big win.

David Chelimsky
Owner

The win isn't just about the syntax you're looking at. There's a bigger picture, which I've explained above: alignment with argument matchers in rspec-mocks (and/or mocha if that's your preference). A kind_of matcher works in any of these situations:

expect(result, kind_of(Thing))
expect(kind_of(Thing), result)
expect(kind_of(Thing)) { do_something }
foo.should_receive(:bar).with(kind_of(Thing))

It does not, however, work with

expect(result).to kind_of(Thing)

As for readability, I actually find the more terse expect(e) { a } every bit as readable - in fact more readable since there is less noise.

As for expect(actual, expected) vs expect(expected, actual), vs expect(expected) { actual }, I tend toward the latter because a) it is a more correct use of the word expect (see below) and b) the first two create the same confusion as assert_equal does (which arg comes first?).

From http://dictionary.reference.com/browse/expect:

ex·pect   [ik-spekt] Show IPA
verb (used with object)

  1. to look forward to; regard as likely to happen; anticipate the occurrence or the coming of: I expect to read it. I expect him later. She expects that they will come.
  2. to look for with reason or justification: We expect obedience.
  3. Informal . to suppose or surmise; guess: I expect that you are tired from the trip.
  4. to anticipate the birth of (one's child): Paul and Sylvia expect their second very soon.

All of the examples follow the word expect with the expected thing. In all but the 3rd (informal) definition, "expect" suggests something that will happen in the future. With that in mind, I think that

expect(3) { calculator.add(1, 2) }

... is a more correct use of the word "expect" than

expect { calculator.add(1, 2) }.to eq(3)
Justin Ko

Well now that you throw the definition into the mix it makes more sense. The alignment with arg matchers is really nice too. Overall this is cleaner.

How would you like to approach this? Separate gem or built-in?

Zach Dennis

Another aspect related to the expect API brought up recently was the idea of expecting a particular value to be yielded. The original problem is basically betting rid of the temporary variable and making the syntax for succinct then this:

yielded = false
subject.method { yielded = true }
yielded.should == true

expect w/block as-is

#expect with a block won't work nicely since Ruby doesn't treat methods as first class objects, i.e.:

# this won't work since #each will be executed
expect(yields(result)) { obj.some_method }

expect with asset_equal API

The following syntax would work but suffers from the ambiguity problem that assert_equal has (as you mentioned above):

# reading this left to right actual makes sense
expect obj, :some_method, yields(result)

# this is more akin to "colorless green ideas sleep furiously"
expect yields(result), obj, :some_method

expect w/block and #to

The #to syntax could work as an alternative, but then #yields has got to go because it makes no sense:

# this doesn't quite work left to right
expect(obj, :some_method).to yields(value)

# this works better but overriding yield is full of peril best avoided
expect(obj, :some_method).to yield(value)

# this works better but it a bit verbose
expect(obj, :some_method).to yield_value(value)

If the expect/to combination is supported this makes sense because it's aligned with everything else that uses #to. However, if expect/to doesn't make it for anything else it seems weird to support it for this one off variation. Let's keep digging.

Maybe expect(yields(value)) ?

Right now, #expect(expected, &blk) seems to be preferred. However, we might be able to find something that is in alignment with this. First attempt might be to rely on the convention of #subject and then to detach the method in the block:

expect(yields(value)){ subject.method(:foo) }

Sadly, this won't work for methods that take any arguments. Limited by the fact that ruby doesn't treat methods as first class citizens, we can't eloquently curry or partially apply arguments to #foo so I don't think this is a good approach.

Maybe expect w/capture (my fav)?

Another possibility is to push the capturing of the yield into a special method inside the block rather than as a part of the expected value, i.e.:

expect(value){ capture_yield(subject, :foo) }
expect(value){ capture_yield(subject, :foo, arg1, arg2) }

# more succinct version if we so dare
expect(value){ capture(subject, :foo) }

I actually really like this variation. It is consistent with expect(value, &blk), allows any number of arguments, doesn't require any one-off syntax like expect/to, and avoids the ambiguity problem of the assert_equal style interface.

Summary

This comment may better be suited for its own feature request, but since we're talking about the #expect API it seemed relevant to add this perspective to the discussion.

Thoughts?

Myron Marston

I'm still a bit cool on your proposed syntax, @dchelimsky. A few random thoughts:

  • You mention a primary benefit being the alignment of the "voice" with mock argument matchers. This is true, but in the 2-3 years I've been using RSpec I've never once needed to write an argument matcher, so this sounds more useful in theory than in practice (at least, for how I write tests). Plus, getting the right "voice" with the existing syntax (and/or my original proposed expect syntax) is as simple as defining the matcher one way, then using alias to define a corresponding argument matcher (e.g. alias multiple_of be_multiple_of).
  • It's largely about ROI for me. What you're proposing sounds like a total rewrite of rspec-expectations. That's a huge investment to make. I had this one pain point with rspec-expectations (a delegator proxying should to the delegated object, leading to unexpectedly failing expectations) that switching to expect(something).to ... would solve with very minimal investment. Other than that, I'm really quite happy with rspec-expectations as it is :).
  • There's also the learning curve involved w/ your new proposed syntax. RSpec users would have to learn a whole new set of matchers. expect(something).to ... has essentially zero learning curve since the existing matchers all continue to work.

It's clear your fond of the syntax, though. I imagine others will be as well. And who knows, it may totally grow on me someday :). For now, I have hard time justifying the ROI and learning curve for such a big change. The benefits are very minimal to me.

If RSpec does move in this new direction, do you expect it to be part of rspec-expectations? Or potentially a new alternate expectation library? It's nice that rspec-core easily supports picking an expectation/assertion library of your choice!

Also, is there any reason we can't consider doing both of the proposals so far?

  • Add support for the expect(something).to ... syntax to rspec-expectations.
  • Begin work on an alternate expectation library based on your proposed syntax.

I guess I kinda see them as separate questions: doing one does not preclude doing the other or both.

@zdennis -- take my feedback here with a grain of salt since I've never particularly wanted a yield matcher, but wouldn't this work?

expect { some_object.some_method(:some_argument) }.to yield_value(value)

Yielding is akin to throwing or raising an error to me: it's something you have to run a piece of code to observe. Hence, it feels like it belongs as a block expectation. Several of your ideas above just use :some_method as an argument, but that doesn't seem to support methods that require arguments very well.

Zach Dennis

@myronmarston (re: yield_value) that syntax won't work because it doesn't allow a block to be passed that can capture and ensure the yielded value is the expected value. In the example you posted when the expect block is executed so is some_object.some_method, so you lose the ability to pass in a block to capture the yielded value.

If expect/to is kept then this could be implemented like this:

expect(some_object, :some_method, arg1, arg2).to yield_value(value)

Another option would be something similar to what was mentioned in the March 2012 thread:

expect(some_object, :some_method, arg1, arg2).to_yield value

The expect/capture example in my earlier comment addresses passing arguments to the method. Also, a few of the other examples specifically mention their inability to do that as a limitation which causes them to not be serious possibilities.

I think no matter what route the #expect API goes we have good possibilities all the way around:

# i like these options if expect/to syntax adopted
expect(some_object, :some_method, arg1, arg2).to yield_value(value)
expect(some_object, :some_method, arg1, arg2).to_yield value

# if expect/to is not adopted or if new expectations library is created
expect(value){ capture_yield(subject, :foo, arg1, arg2) }

I realize that my commentary now may be detracting from the discussion at hand. I'll open another issue for the "should yield value".

David Chelimsky
Owner

@myronmarston I'm cold on making any changes/additions whatsoever! Everything we add to rspec, we have to live with for a long time.

I definitely don't have the cycles in the near term to roll out an alternate matcher library and, TBPH, I'm not convinced (in spite of my arguments to the contrary - hey - I can waffle, I'm not running for office) that it would be that valuable to do so. As you point out ppl can already choose rspec-expectations, t/u assertions, or any other lib whose assertions raise errors (assert2, wrong, any others?).

Let's trim this conversation down to the syntax proposed in the initial issue (way, way up ... keep scrolling ...). We still have the open issue of one liners. I'd propose that we don't need to abandon the word should entirely even if we offer a config option to not add should to object. In that case, it { should do_something } would delegate directly to a handler (which I've already committed to rspec-core: rspec/rspec-core@b93433d.

If we can settle on what to do w/ one liners, I'm good to go on this on the basis that it paves the way for eliminating the global should.

Myron Marston

@dchelimsky -- sounds good. The change in rspec-core is good and this seems like the right approach for one liners.

I'll take a first-pass at adding a config option to opt-out of should.

Myron Marston

OK, my first pass at the config option is here.

  • Would it be better to use remove_method :should; remove_method :should_not rather than the undefs? I can't think of a case where it would matter here...
  • I added a config option both to rspec-expectations directly (for folks who use it w/o rspec-core) and to the rspec-core config API.
David Chelimsky
Owner

@myronmarston I'd rather add those methods if the configuration says to (which it would do by default) than remove them. Any reason not to approach it that way?

Myron Marston myronmarston referenced this pull request in rspec/rspec-core
Closed

in_sub_process helper #583

Myron Marston

@myronmarston I'd rather add those methods if the configuration says to (which it would do by default) than remove them. Any reason not to approach it that way?

I'd rather do that, too, but for backwards-compatibility reasons I think it needs to be done this way. Consider the case where someone is using rspec-expectations w/o rspec-core. Their test suite will have require "rspec/expectations" somewhere, and given that that is all that is needed to make object.should matcher available, it needs to continue to be all that's needed--until we decide to switch to opt-in rather than opt-out. Given that RSpec::Expecations.enable_should_and_should_not = true may never be called (since it's the default, after all), there isn't any point I can think of to add should and should_not before their tests/examples start running.

Maybe I'm missing something, though...can you think of a way to make this opt-out while keeping full backwards compatibility?

If we go with the approach of my commit, I figure that we can change it around to how you suggest once we switch to opt-in.

David Chelimsky
Owner

Consider the case where someone is using rspec-expectations w/o rspec-core.

Does anybody actually do that?

FYI - I just tried this on this branch:

RSpec.configure {|c| c.enable_should_and_should_not = false }

describe "something" do
  it "does something" do
    expect(5).to equal(5)
  end
end

and got this failure:

/Users/david/projects/ruby/rspec2/repos/rspec-expectations/example_spec.rb:1:in `block in <top (required)>': undefined method `enable_should_and_should_not=' for #<RSpec::Core::Configuration:0x007fb0a9a3c7d8> (NoMethodError)
    from /Users/david/projects/ruby/rspec2/repos/rspec-core/lib/rspec/core.rb:94:in `configure'
    from /Users/david/projects/ruby/rspec2/repos/rspec-expectations/example_spec.rb:1:in `<top (required)>'
    from /Users/david/projects/ruby/rspec2/repos/rspec-core/lib/rspec/core/configuration.rb:746:in `load'
    from /Users/david/projects/ruby/rspec2/repos/rspec-core/lib/rspec/core/configuration.rb:746:in `block in load_spec_files'
    from /Users/david/projects/ruby/rspec2/repos/rspec-core/lib/rspec/core/configuration.rb:746:in `map'
    from /Users/david/projects/ruby/rspec2/repos/rspec-core/lib/rspec/core/configuration.rb:746:in `load_spec_files'
    from /Users/david/projects/ruby/rspec2/repos/rspec-core/lib/rspec/core/command_line.rb:22:in `run'
    from /Users/david/projects/ruby/rspec2/repos/rspec-core/lib/rspec/core/runner.rb:69:in `run'
    from /Users/david/projects/ruby/rspec2/repos/rspec-core/lib/rspec/core/runner.rb:10:in `block in autorun'
David Chelimsky
Owner

@myronmarston I'd like to get this branch up to date with the latest changes in master. I tried rebasing off master and merging in master locally - I prefer the outcome of rebasing - got a preference?

Myron Marston

@myronmarston I'd like to get this branch up to date with the latest changes in master. I tried rebasing off master and merging in master locally - I prefer the outcome of rebasing - got a preference?

I just rebased and forced push. I definitely prefer rebasing for a branch like this.

David Chelimsky
Owner

Great. Thanks.

Myron Marston

Does anybody actually do that?

I don't know, but wasn't that one of the reasons to extract this into its own gem, so people can do that now? Besides that, even for RSpec users, I'm not sure how to make this work to be opt-out but only add should and should_not to kernel if the user does not configure enable_should_and_should_not = false. Consider this simple spec:

describe "something" do
  it "can use should" do
    5.should eq(5)
  end
end

When this is run as a standalone spec, at which point would we add should and should_not to Kernel? Since they haven't explicitly configured anything, there's no point in the configuration I can think of that would allow us to know we can add those methods. The only way I can think of to switch this is far hackier than what I have here: define method_missing on kernel, and if should is called and the config option hasn't been set to false, add the methods.

Myron Marston

FYI - I just tried this on this branch:
...
and got this failure:

Nice catch. I have to admit, I didn't try it yesterday beyond seeing the specs fail and making them pass. The problem is that rspec-expectations isn't immediately loaded by rspec-core--unless you explicitly set expect_with we wait until right when the first example group is defined. For now, this works:

RSpec.configure do |c|
  c.expect_with :rspec
  c.enable_should_and_should_not = false
end

describe "something" do
  it "does something" do
    expect(5).to equal(5)
  end
end

That's obviously not ideal and shouldn't be what we release this with. I'm not sure what the best way is to fix this yet...

David Chelimsky
Owner

I think this syntax should not support operator matchers (see #138).

Pat Maddox

It appears that with this discussion RSpec's syntax might be coming full-circle:

expect(actual, expected) vs expect(expected, actual), vs expect(expected) { actual }

gsub('expect', 'assert_equal')

:)

David Chelimsky
Owner

Funny. I've actually finally learned how to read assert_equal so I always get the order right thanks, in part, to partial evaluation in functional programming. assert_equal 3, actual becomes assert_equal_3 actual :)

Myron Marston
Owner

Getting back into this issue (after a bit of a break; I got a bit burned out after doing the work on the yield matchers...), there are a few things we need to resolve before this is ready to merge:

  • The mechanism for adding/removing should and should_not: is there a way to wait to add these methods, or do we add them by default, and then remove them if so configured? I'd prefer the former but can't find a way to do it without a horrible method_missing hack, so for now the implementation does the latter.
  • Configuration: a config option is available as RSpec::Expectations.enable_should_and_should_not = false (which works well when rspec-expectations is used stand alone), but there isn't yet a good config option available from RSpec.configure for typical rspec usage. There's code in rspec-expectations that will add a config option to the rspec-core configuration object, but it only works if the user has manually required rspec-expectations or explicitly configured config.expect_with :rspec before trying to configure enable_should_and_should_not--so it's less than ideal. Any ideas how to improve this?
  • Operator matchers: should these be supported for this syntax? This is an open question (#138 has the main conversation)

Thoughts from anyone on these issues?

David Chelimsky
Owner

Operator matchers: should these be supported for this syntax?

IMO, no. Introducing a new syntax gives us an opportunity to simplify things, and operator matchers are complicated (look at what should and should_not have to do to handle them).

David Chelimsky
Owner

The mechanism for adding/removing should and should_not: is there a way to wait to add these methods, or do we add them by default, and then remove them if so configured? I'd prefer the former but can't find a way to do it without a horrible method_missing hack, so for now the implementation does the latter.

Which is worse - trying to find where a defined method got removed for trying to find where a missing method got defined? :)

Why don't we configure which syntax to include like this:

RSpec::Expectations.syntax = :should
RSpec::Expectations.syntax = :expect
RSpec::Expectations.syntax = [:should, :expect]

The default for now would be [:should, :expect], and RSpec.configure can delegate the same setting if both libs are being used:

RSpec.configure do |c|
  c.expectation_syntax = :should
  c.expectation_syntax = :expect
  c.expectation_syntax = [:should, :expect]
end

Or maybe (requires a bit more thought and work):

RSpec.configure do |c|
  c.expect_with :rspec do |expectations|
    expectations.syntax = :should
    expectations.syntax = :expect
    expectations.syntax = [:should, :expect]
  end
end
Justin Ko

IMO, no. Introducing a new syntax gives us an opportunity to simplify things, and operator matchers are complicated (look at what should and should_not have to do to handle them).

So if they choose to use expect, then they won't be able to "assert" on a number being greater than another number? If we don't offer an alternative to operator matchers, I think some users will revert back to should.

Maybe adding greater_than type matchers would help with this. The only other way I can think of would be to do something like this:

expect(6 > 5).to be_true

Yuck.

Myron Marston
Owner

Which is worse - trying to find where a defined method got removed for trying to find where a missing method got defined? :)

I'm not sure that one is easier to find than the other. I have a hard time stating why, but defining method_missing on Kernel feels really hacky and super invasive, and I'd like to avoid it. We'd be fundamentally messing with how every object in the system responds to messages. Maybe I'm overreacting but "with great power comes great responsibility" and doing this scares me a bit. Do you have specific concerns about the undef approach?

Why don't we configure which syntax to include like this

I like your suggested configuration syntax quite a bit. The tricky bit is how we make the config option available from RSpec.configure while keeping rspec-core and rspec-expectations decoupled. For a syntax like RSpec.configure { |c| c.expectation_syntax = :expect } to work, expectation_syntax would have to be defined directly in rspec-core (because we can't count on rspec-expectations being pre-loaded), but that feels like something core should have no knowledge of. On the other hand, your expect_with block syntax would work well here to keep things decoupled...with that approach, there's no need to pollute RSpec::Configuration with rspec-expectations-specific config options.

Maybe adding greater_than type matchers would help with this. The only other way I can think of would be to do something like this:

Actually, there are already matchers for these:

expect(6).to be > 5

be is a matcher that supports operators being changed off of it for comparisons, so no problem there.

There is one operator matcher that doesn't currently have any normal equivalent, though: the =~ operator matcher for arrays. I think we'd need to come up with a named matcher for it (maybe something like expect(array).to have_some_elements_as(1, 2, 3)?) before removing operator matcher support from the expect syntax.

Justin Ko

be is a matcher that supports operators being changed off of it for comparisons, so no problem there.

How is "stand-alone" operator matchers not deprecated yet? We could get rid of the "should can accept no args" troubles.

Thanks for the correction, didn't know be could do that.

I think we'd need to come up with a named matcher for it (maybe something like expect(array).to have_some_elements_as(1, 2, 3)?)

We could name it by its internal name: "match_array"

David Chelimsky
Owner

How is "stand-alone" operator matchers not deprecated yet?

I don't think we should. There is too much x.should == y out in the world (including in RSpec's own specs).

David Chelimsky
Owner

How about expect([1,2,3]).to eq_unordered [3,1,2]?

David Chelimsky
Owner

@myronmarston FYI I'm working on yielding a config:

config.expect_with :rspec do |custom_config|
  custom_config.custom_setting = true
end

Should have that pushed to rspec-core tonight.

Myron Marston
Owner

How about expect([1,2,3]).to eq_unordered [3,1,2]?

That works OK. I like that it's explicit but I don't think it reads very well.

Should have that pushed to rspec-core tonight.

Cool, thanks for taking care of that. I'll try to tackle having the expect syntax not support operator matchers soon.

David Chelimsky
Owner

How about expect([1,2,3]).to eq_unordered [3,1,2]?

That works OK. I like that it's explicit but I don't think it reads very well.

How about match_set ?

Myron Marston
Owner

How about match_set ?

I like that--the semantics of a set are really what the matcher is going for anyway.

Actually, maybe not--a set implies no duplicates, and we allow duplicates. So it's almost like a set but not quite...

David Chelimsky
Owner

When we came up with =~ we had played around with a number of ideas and didn't like any of them (I'll link to the issue if can find it), so this word-search is not new :(

Tom Stuart

FWIW, a set with duplicates is called a "multiset" or "bag". Not sure how well match_bag rolls off the tongue.

Zach Dennis

I find unordered equivalence hard to describe without being overly verbose or introducing new terms which aren't in common use because we typically don't think that way or solve problems (except for sets but those are different than what's being talked about). Maybe finding order first and then comparing will be more natural and intuitive.

Right now for full matching I typically use #sort as my expected is usually sorted to begin with:

# most of the time my expected is already sorted
expect([3,1,2].sort).to eq([1,2,3])

And if not I end up with:

expect([3,1,2].sort).to eq([2,1,3].sort)
expect([3,1,2].sort).to eq [2,1,3].sort

I don't mind the above because it's very clear and easy to read and the language is practically universal, we all know what sort does. Maybe to clean it up you end up with something like #match_sorted:

expect([3,1,2]).to match_sorted([1,2,3])

It doesn't save keystrokes, but is a little bit clear by avoiding unnecessary method calls and dot operators.

Another 2 cents on the pile. :)

Myron Marston
Owner

@dchelimsky thanks! That looks great.

Another idea for =~: expect([1, 2, 3]).to have_all_of([2, 3, 1])

myronmarston added some commits
Myron Marston myronmarston Add support for `expect(value)` syntax.
Note: there's more to do here (documentation, etc); this is just a starting point for discussion and comments.
d279c3b
Myron Marston myronmarston Don't expose #target as a public API.
Also, fix the spelling of the class while I'm at it.
0d7ce9c
Myron Marston myronmarston Add configuration API for choosing an expectation syntax.
:should, :expect or both can be chosen.
d9ab1a3
Myron Marston
Owner

I rebased (to get things current again) and implemented @dchelimsky's suggested config API. It was actually fairly easy to make it revertible as well.

I'll tackle disabling operator matchers next.

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request fails (merged d9ab1a3 into 2e348ff).

Myron Marston
Owner

Woah, that travis notification is pretty sweet. I'll tackle fixing it tomorrow.

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request fails (merged ad2a757 into 2e348ff).

Myron Marston myronmarston Refactor enabling/disabling of expectation syntaxes.
* Fix build on JRuby. Our sandboxing via forking didn't work
  on JRuby since fork isn't available. On JRuby we just
  re-enable all syntaxes at the end of each sandboxed example.
* Testing this revealed that the way I was restoring a disabled
  syntax didn't always work. Based on the random order, sometimes
  spec/rspec/matchers/be_spec.rb:427 would fail with
  "TypeError: bind argument must be an instance of Kernel".
* Refactored main logic into new syntax module, that can add
  the syntaxes to any class or module. Kernel/RSpec::Matcher
  defaults are provided for convenience. This also fixes the
  bind failure, by redefining the methods anew rather than
  re-binding the old ones.
309c4f0
Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged 309c4f0 into 2e348ff).

Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged 6de81c7 into 2e348ff).

Justin Ko

That's one convention for use of the bang...another (that I've tended to favor) is that the bang means "use with care", and with this convention a method that exists only to raise an exception (such as this one) should be used with care.

That said, I don't feel particularly strongly about it.

Myron Marston myronmarston Cleanup code a bit.
- Remove bang from method...as @justinko rightly pointed out, there's no corresponding bangless method so it didn't really follow convention here.
- Use an early guarded return.
8567ac6
Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged 8567ac6 into 2e348ff).

Myron Marston

@justinko -- I just removed the bang. Thinking about it some more, I like the convention that blog post mentions. I've never thought very consciously about my bang usage.

BTW, as the code stands now, when you do this:

RSpec.configure do |rspec|
  rspec.expect_with :rspec do |c|
    c.syntax = :should
  end
end

...then this will be disallowed:

it 'tries to use the expect block syntax' do
  expect { something }.to raise_error
end

Prior to this pull request, expect could be used with a block as a more-readable alternative than lambda { }.should. Disabling the expect syntax causes all uses of expect to be disabled, not simply for the new functionality. I wasn't sure about doing it this way originally but it was far simpler than syntax = :should preventing expect(something) but allowing expect { something }, and it's more consistent...if the user tells rspec "I don't want to use the expect syntax" then I figure it makes sense to disable it entirely.

Another thing...RSpec::Expectations::Syntax.enable_should(DelegateClass) can now be used to deal with issues like #114, but I'm not sure if we should make that an official public API. I'm leaning towards marking it as a private API, and encouraging users to use the new expect syntax as the supported solution to those kinds of issues.

Finally, @justinko suggested match_array above as the matcher replacement for =~ and the more I think about it, the more I like it. Any opposition to using that as the name?

@dchelimsky, @justinko, or anyone else: thoughts on any of these?

Justin Ko

Agreed on all points from me.

myronmarston added some commits
Myron Marston myronmarston Add yard docs for new modules.
[ci skip]
f1b8aa2
Myron Marston myronmarston Add match_array matcher method for the old =~ array matcher.
This is needed because we've decided not to support operator matchers off of `expect(value).to`, and `match_array` is the best name we've come up with for it.
f00de57
Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged f00de57 into 3cf9110).

Myron Marston

@dchelimsky -- anything else you want changed before I merge this in?

David Chelimsky
Owner

@myronmarston nope - have at it.

Myron Marston myronmarston merged commit 71a2c8d into from
David Chelimsky

@myronmarston unfortunately rdoc (or yard) doesn't pick up this documentation since it's not right in a module. Including a module that defines should and should_not doesn't work either, so I'm at a loss. It's pretty crucial that these methods are in the rdoc and I'd rather not fake it if possible. Any ideas?

I don't know rdoc/yard very well, but would something like this work? It looks like there are directives in the comments (e.g. :method:) that we may be able to use...

Actually it does work if we include such a module on BasicObject :)

Myron Marston myronmarston referenced this pull request in rspec/rspec-core
Closed

Enhance expect syntax expressiveness #953

Coveralls

Coverage Status

Changes Unknown when pulling f00de57 on expect_syntax into ** on master**.

Jon Rowe
Owner

Lol coveralls bot. Although it has drawn my attention to the fact the branch still exists, can it be safely deleted now @myronmarston?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on May 9, 2012
  1. Myron Marston

    Add support for `expect(value)` syntax.

    myronmarston authored
    Note: there's more to do here (documentation, etc); this is just a starting point for discussion and comments.
  2. Myron Marston

    Don't expose #target as a public API.

    myronmarston authored
    Also, fix the spelling of the class while I'm at it.
  3. Myron Marston

    Add configuration API for choosing an expectation syntax.

    myronmarston authored
    :should, :expect or both can be chosen.
  4. Myron Marston
Commits on May 10, 2012
  1. Myron Marston

    Refactor enabling/disabling of expectation syntaxes.

    myronmarston authored
    * Fix build on JRuby. Our sandboxing via forking didn't work
      on JRuby since fork isn't available. On JRuby we just
      re-enable all syntaxes at the end of each sandboxed example.
    * Testing this revealed that the way I was restoring a disabled
      syntax didn't always work. Based on the random order, sometimes
      spec/rspec/matchers/be_spec.rb:427 would fail with
      "TypeError: bind argument must be an instance of Kernel".
    * Refactored main logic into new syntax module, that can add
      the syntaxes to any class or module. Kernel/RSpec::Matcher
      defaults are provided for convenience. This also fixes the
      bind failure, by redefining the methods anew rather than
      re-binding the old ones.
  2. Myron Marston
Commits on May 12, 2012
  1. Myron Marston

    Cleanup code a bit.

    myronmarston authored
    - Remove bang from method...as @justinko rightly pointed out, there's no corresponding bangless method so it didn't really follow convention here.
    - Use an early guarded return.
  2. Myron Marston

    Add yard docs for new modules.

    myronmarston authored
    [ci skip]
  3. Myron Marston

    Add match_array matcher method for the old =~ array matcher.

    myronmarston authored
    This is needed because we've decided not to support operator matchers off of `expect(value).to`, and `match_array` is the best name we've come up with for it.
This page is out of date. Refresh to see the latest.
2  lib/rspec/expectations.rb
View
@@ -1,5 +1,7 @@
require 'rspec/expectations/extensions'
require 'rspec/matchers'
+require 'rspec/matchers/configuration'
+require 'rspec/expectations/expectation_target'
require 'rspec/expectations/fail_with'
require 'rspec/expectations/errors'
require 'rspec/expectations/deprecation'
52 lib/rspec/expectations/expectation_target.rb
View
@@ -0,0 +1,52 @@
+module RSpec
+ module Expectations
+ # Wraps the target of an expectation.
+ # @example
+ # expect(something) # => ExpectationTarget wrapping something
+ class ExpectationTarget
+ # @api private
+ def initialize(target)
+ @target = target
+ end
+
+ # Runs the given expectation, passing if `matcher` returns true.
+ # @example
+ # expect(value).to eq(5)
+ # expect { perform }.to raise_error
+ # @param [Matcher]
+ # matcher
+ # @param [String] message optional message to display when the expectation fails
+ # @return [Boolean] true if the expectation succeeds (else raises)
+ # @see RSpec::Matchers
+ def to(matcher=nil, message=nil, &block)
+ prevent_operator_matchers(:to, matcher)
+ RSpec::Expectations::PositiveExpectationHandler.handle_matcher(@target, matcher, message, &block)
+ end
+
+ # Runs the given expectation, passing if `matcher` returns false.
+ # @example
+ # expect(value).to_not eq(5)
+ # expect(value).not_to eq(5)
+ # @param [Matcher]
+ # matcher
+ # @param [String] message optional message to display when the expectation fails
+ # @return [Boolean] false if the negative expectation succeeds (else raises)
+ # @see RSpec::Matchers
+ def to_not(matcher=nil, message=nil, &block)
+ prevent_operator_matchers(:to_not, matcher)
+ RSpec::Expectations::NegativeExpectationHandler.handle_matcher(@target, matcher, message, &block)
+ end
+ alias not_to to_not
+
+ private
+
+ def prevent_operator_matchers(verb, matcher)
+ return if matcher
+
+ raise ArgumentError, "The expect syntax does not support operator matchers, " +
+ "so you must pass a matcher to `##{verb}`."
+ end
+ end
+ end
+end
+
1  lib/rspec/expectations/extensions.rb
View
@@ -1,3 +1,2 @@
-require 'rspec/expectations/extensions/kernel'
require 'rspec/expectations/extensions/array'
require 'rspec/expectations/extensions/object'
26 lib/rspec/expectations/extensions/kernel.rb
View
@@ -1,26 +0,0 @@
-module Kernel
- # Passes if +matcher+ returns true. Available on every +Object+.
- # @example
- # actual.should eq(expected)
- # actual.should be > 4
- # @param [Matcher]
- # matcher
- # @param [String] message optional message to display when the expectation fails
- # @return [Boolean] true if the expectation succeeds (else raises)
- # @see RSpec::Matchers
- def should(matcher=nil, message=nil, &block)
- RSpec::Expectations::PositiveExpectationHandler.handle_matcher(self, matcher, message, &block)
- end
-
- # Passes if +matcher+ returns false. Available on every +Object+.
- # @example
- # actual.should_not eq(expected)
- # @param [Matcher]
- # matcher
- # @param [String] message optional message to display when the expectation fails
- # @return [Boolean] false if the negative expectation succeeds (else raises)
- # @see RSpec::Matchers
- def should_not(matcher=nil, message=nil, &block)
- RSpec::Expectations::NegativeExpectationHandler.handle_matcher(self, matcher, message, &block)
- end
-end
91 lib/rspec/expectations/syntax.rb
View
@@ -0,0 +1,91 @@
+module RSpec
+ module Expectations
+ # @api private
+ # Provides methods for enabling and disabling the available
+ # syntaxes provided by rspec-expectations.
+ module Syntax
+ extend self
+
+ # @api private
+ # Enables the `should` syntax.
+ def enable_should(syntax_host = ::Kernel)
+ return if should_enabled?(syntax_host)
+
+ syntax_host.module_eval do
+ # Passes if +matcher+ returns true. Available on every +Object+.
+ # @example
+ # actual.should eq(expected)
+ # actual.should be > 4
+ # @param [Matcher]
+ # matcher
+ # @param [String] message optional message to display when the expectation fails
+ # @return [Boolean] true if the expectation succeeds (else raises)
+ # @see RSpec::Matchers
+ def should(matcher=nil, message=nil, &block)
+ RSpec::Expectations::PositiveExpectationHandler.handle_matcher(self, matcher, message, &block)
+ end
+
+ # Passes if +matcher+ returns false. Available on every +Object+.
+ # @example
+ # actual.should_not eq(expected)
+ # @param [Matcher]
+ # matcher
+ # @param [String] message optional message to display when the expectation fails
+ # @return [Boolean] false if the negative expectation succeeds (else raises)
+ # @see RSpec::Matchers
+ def should_not(matcher=nil, message=nil, &block)
+ RSpec::Expectations::NegativeExpectationHandler.handle_matcher(self, matcher, message, &block)
+ end
+ end
+ end
+
+ # @api private
+ # Disables the `should` syntax.
+ def disable_should(syntax_host = ::Kernel)
+ return unless should_enabled?(syntax_host)
+
+ syntax_host.module_eval do
+ undef should
+ undef should_not
+ end
+ end
+
+ # @api private
+ # Enables the `expect` syntax.
+ def enable_expect(syntax_host = ::RSpec::Matchers)
+ return if expect_enabled?(syntax_host)
+
+ syntax_host.module_eval do
+ def expect(*target, &target_block)
+ target << target_block if block_given?
+ raise ArgumentError.new("You must pass an argument or a block to #expect but not both.") unless target.size == 1
+ ::RSpec::Expectations::ExpectationTarget.new(target.first)
+ end
+ end
+ end
+
+ # @api private
+ # Disables the `expect` syntax.
+ def disable_expect(syntax_host = ::RSpec::Matchers)
+ return unless expect_enabled?(syntax_host)
+
+ syntax_host.module_eval do
+ undef expect
+ end
+ end
+
+ # @api private
+ # Indicates whether or not the `should` syntax is enabled.
+ def should_enabled?(syntax_host = ::Kernel)
+ syntax_host.method_defined?(:should)
+ end
+
+ # @api private
+ # Indicates whether or not the `expect` syntax is enabled.
+ def expect_enabled?(syntax_host = ::RSpec::Matchers)
+ syntax_host.method_defined?(:expect)
+ end
+ end
+ end
+end
+
17 lib/rspec/matchers.rb
View
@@ -181,7 +181,6 @@ module Matchers
require 'rspec/matchers/operator_matcher'
require 'rspec/matchers/be_close'
-require 'rspec/matchers/block_aliases'
require 'rspec/matchers/generated_descriptions'
require 'rspec/matchers/method_missing'
require 'rspec/matchers/compatibility'
@@ -664,19 +663,27 @@ def yield_successive_args(*args)
BuiltIn::YieldSuccessiveArgs.new(*args)
end
- # Passes if actual contains all of the expected regardless of order.
- # This works for collections. Pass in multiple args and it will only
+ # Passes if actual contains all of the expected regardless of order.
+ # This works for collections. Pass in multiple args and it will only
# pass if all args are found in collection.
#
- # NOTE: there is no should_not version of array.should =~ other_array
- #
+ # @note This is also available using the `=~` operator with `should`,
+ # but `=~` is not supported with `expect`.
+ # @note There is no should_not version of array.should =~ other_array
+ #
# @example
#
+ # expect([1,2,3]).to match_array([1,2,3])
+ # expect([1,2,3]).to match_array([1,3,2])
# [1,2,3].should =~ [1,2,3] # => would pass
# [1,2,3].should =~ [2,3,1] # => would pass
# [1,2,3,4].should =~ [1,2,3] # => would fail
# [1,2,2,3].should =~ [1,2,3] # => would fail
# [1,2,3].should =~ [1,2,3,4] # => would fail
+ def match_array(array)
+ BuiltIn::MatchArray.new(array)
+ end
+
OperatorMatcher.register(Array, '=~', BuiltIn::MatchArray)
end
end
21 lib/rspec/matchers/block_aliases.rb
View
@@ -1,21 +0,0 @@
-require 'rspec/expectations/extensions/kernel'
-module RSpec
- module Matchers
- module BlockAliases
- alias_method :to, :should
- alias_method :to_not, :should_not
- alias_method :not_to, :should_not
- end
-
- # Extends the submitted block with aliases to and to_not
- # for should and should_not.
- #
- # @example
- # expect { this_block }.to change{this.expression}.from(old_value).to(new_value)
- # expect { this_block }.to raise_error
- def expect(&block)
- block.extend BlockAliases
- end
- end
-end
-
53 lib/rspec/matchers/configuration.rb
View
@@ -0,0 +1,53 @@
+require 'rspec/expectations/syntax'
+
+module RSpec
+ module Matchers
+ # Provides configuration options for rspec-expectations.
+ class Configuration
+ # Configures the supported syntax.
+ # @param [Array<Symbol>, Symbol] values the syntaxes to enable
+ # @example
+ # RSpec.configure do |rspec|
+ # rspec.expect_with :rspec do |c|
+ # c.syntax = :should
+ # # or
+ # c.syntax = :expect
+ # # or
+ # c.syntax = [:should, :expect]
+ # end
+ # end
+ def syntax=(values)
+ if Array(values).include?(:expect)
+ Expectations::Syntax.enable_expect
+ else
+ Expectations::Syntax.disable_expect
+ end
+
+ if Array(values).include?(:should)
+ Expectations::Syntax.enable_should
+ else
+ Expectations::Syntax.disable_should
+ end
+ end
+
+ # The list of configured syntaxes.
+ # @return [Array<Symbol>] the list of configured syntaxes.
+ def syntax
+ syntaxes = []
+ syntaxes << :should if Expectations::Syntax.should_enabled?
+ syntaxes << :expect if Expectations::Syntax.expect_enabled?
+ syntaxes
+ end
+ end
+
+ # The configuration object
+ # @return [RSpec::Matchers::Configuration] the configuration object
+ def self.configuration
+ @configuration ||= Configuration.new
+ end
+
+ # set default syntax
+ configuration.syntax = [:expect, :should]
+ end
+end
+
65 spec/rspec/expectations/expectation_target_spec.rb
View
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+module RSpec
+ module Expectations
+ # so our examples below can set expectations about the target
+ ExpectationTarget.send(:attr_reader, :target)
+
+ describe ExpectationTarget do
+ context 'when constructed via #expect' do
+ it 'constructs a new instance targetting the given argument' do
+ expect(7).target.should eq(7)
+ end
+
+ it 'constructs a new instance targetting the given block' do
+ block = lambda {}
+ expect(&block).target.should be(block)
+ end
+
+ it 'raises an ArgumentError when given an argument and a block' do
+ lambda { expect(7) { } }.should raise_error(ArgumentError)
+ end
+
+ it 'raises an ArgumentError when given neither an argument nor a block' do
+ lambda { expect }.should raise_error(ArgumentError)
+ end
+
+ it 'can be passed nil' do
+ expect(nil).target.should be_nil
+ end
+
+ it 'passes a valid positive expectation' do
+ expect(5).to eq(5)
+ end
+
+ it 'passes a valid negative expectation' do
+ expect(5).to_not eq(4)
+ expect(5).not_to eq(4)
+ end
+
+ it 'fails an invalid positive expectation' do
+ lambda { expect(5).to eq(4) }.should fail_with(/expected: 4.*got: 5/m)
+ end
+
+ it 'fails an invalid negative expectation' do
+ message = /expected 5 not to be a kind of Fixnum/
+ lambda { expect(5).to_not be_a(Fixnum) }.should fail_with(message)
+ lambda { expect(5).not_to be_a(Fixnum) }.should fail_with(message)
+ end
+
+ it 'does not support operator matchers from #to' do
+ expect {
+ expect(3).to == 3
+ }.to raise_error(ArgumentError)
+ end
+
+ it 'does not support operator matchers from #not_to' do
+ expect {
+ expect(3).not_to == 4
+ }.to raise_error(ArgumentError)
+ end
+ end
+ end
+ end
+end
+
141 spec/rspec/matchers/configuration_spec.rb
View
@@ -0,0 +1,141 @@
+require 'spec_helper'
+
+module RSpec
+ module Matchers
+ describe ".configuration" do
+ it 'returns a memoized configuration instance' do
+ RSpec::Matchers.configuration.should be_a(RSpec::Matchers::Configuration)
+ RSpec::Matchers.configuration.should be(RSpec::Matchers.configuration)
+ end
+ end
+
+ shared_examples_for "configuring the expectation syntax" do
+ # We want a sandboxed method that ensures that we wind up with
+ # both syntaxes properly enabled when the example ends.
+ #
+ # On platforms that fork, using a sub process is the easiest,
+ # most robust way to achieve that.
+ #
+ # On jRuby we just re-enable both syntaxes at the end of the example;
+ # however, this is a generally inferior approach because it depends on
+ # the code-under-test working properly; if it doesn't work properly,
+ # it could leave things in a "broken" state where tons of other examples fail.
+ if RUBY_PLATFORM == "java"
+ def sandboxed
+ yield
+ ensure
+ configure_syntax([:should, :expect])
+ end
+ else
+ include InSubProcess
+ alias sandboxed in_sub_process
+ end
+
+ it 'is configured to :should and :expect by default' do
+ configured_syntax.should eq([:should, :expect])
+
+ 3.should eq(3)
+ 3.should_not eq(4)
+ expect(3).to eq(3)
+ end
+
+ it 'can limit the syntax to :should' do
+ sandboxed do
+ configure_syntax :should
+ configured_syntax.should eq([:should])
+
+ 3.should eq(3)
+ 3.should_not eq(4)
+ lambda { expect(6).to eq(6) }.should raise_error(NameError)
+ end
+ end
+
+ it 'is a no-op when configured to :should twice' do
+ sandboxed do
+ ::Kernel.stub(:method_added).and_raise("no methods should be added here")
+
+ configure_syntax :should
+ configure_syntax :should
+ end
+ end
+
+ it 'can limit the syntax to :expect' do
+ sandboxed do
+ configure_syntax :expect
+ expect(configured_syntax).to eq([:expect])
+
+ expect(3).to eq(3)
+ expect { 3.should eq(3) }.to raise_error(NameError)
+ expect { 3.should_not eq(3) }.to raise_error(NameError)
+ end
+ end
+
+ it 'is a no-op when configured to :expect twice' do
+ sandboxed do
+ RSpec::Matchers.stub(:method_added).and_raise("no methods should be added here")
+
+ configure_syntax :expect
+ configure_syntax :expect
+ end
+ end
+
+ it 'can re-enable the :should syntax' do
+ sandboxed do
+ configure_syntax :expect
+ configure_syntax [:should, :expect]
+ configured_syntax.should eq([:should, :expect])
+
+ 3.should eq(3)
+ 3.should_not eq(4)
+ expect(3).to eq(3)
+ end
+ end
+
+ it 'can re-enable the :expect syntax' do
+ sandboxed do
+ configure_syntax :should
+ configure_syntax [:should, :expect]
+ configured_syntax.should eq([:should, :expect])
+
+ 3.should eq(3)
+ 3.should_not eq(4)
+ expect(3).to eq(3)
+ end
+ end
+ end
+
+ describe "configuring rspec-expectations directly" do
+ it_behaves_like "configuring the expectation syntax" do
+ def configure_syntax(syntax)
+ RSpec::Matchers.configuration.syntax = syntax
+ end
+
+ def configured_syntax
+ RSpec::Matchers.configuration.syntax
+ end
+ end
+ end
+
+ describe "configuring using the rspec-core config API" do
+ it_behaves_like "configuring the expectation syntax" do
+ def configure_syntax(syntax)
+ RSpec.configure do |rspec|
+ rspec.expect_with :rspec do |c|
+ c.syntax = syntax
+ end
+ end
+ end
+
+ def configured_syntax
+ RSpec.configure do |rspec|
+ rspec.expect_with :rspec do |c|
+ return c.syntax
+ end
+ end
+ end
+ end
+ end
+
+ end
+end
+
12 spec/rspec/matchers/match_array_spec.rb
View
@@ -14,6 +14,18 @@ def ==(other)
end
end
+describe "using match_array with expect" do
+ it "passes a valid positive expectation" do
+ expect([1, 2]).to match_array [2, 1]
+ end
+
+ it "fails an invalid positive expectation" do
+ expect {
+ expect([1, 2, 3]).to match_array [2, 1]
+ }.to fail_with(/expected collection contained/)
+ end
+end
+
describe "array.should =~ other_array" do
it "passes if target contains all items" do
[1,2,3].should =~ [1,2,3]
31 spec/support/in_sub_process.rb
View
@@ -0,0 +1,31 @@
+module InSubProcess
+ # Useful as a way to isolate a global change to a subprocess.
+ def in_sub_process
+ readme, writeme = IO.pipe
+
+ pid = Process.fork do
+ value = nil
+ begin
+ yield
+ rescue => e
+ value = e
+ end
+
+ writeme.write Marshal.dump(value)
+
+ readme.close
+ writeme.close
+ exit! # prevent at_exit hooks from running (e.g. minitest)
+ end
+
+ writeme.close
+ Process.waitpid(pid)
+
+ if exception = Marshal.load(readme.read)
+ raise exception
+ end
+
+ readme.close
+ end
+end
+
Something went wrong with that request. Please try again.