rspec / rspec-mocks

RSpec's 'test double' framework, with support for stubbing and mocking
https://rspec.info
MIT License
1.16k stars 357 forks source link

Use the new expectation syntax for mocks #153

Closed iain closed 11 years ago

iain commented 12 years ago

If you want to use the new expectation syntax, then the current mock syntax will look out of place.

That's why I propose we introduce the following matcher:

expect(object).to receive(:message).with(arg1, arg2)

Some considerations:

myronmarston commented 12 years ago

This would help solve #116, as it's essentially the same delegate issue we've now dealt with in rspec-expectations.

This would not be usable without rspec-expectations. I don't know how problematic this is, but it is a feature of rspec-mocks.

I can think of some ways to make this work w/o depending on rspec-expectations using feature detection.

justinko commented 12 years ago

Basically, RR[1] provides the cleanest solution to this issue. I would like to see something like this in rspec-mocks:

stub(User).all { the_users } # uses `method_missing`
stub(User, :all) { the_users } # does not use `method_missing`

mock(User).all # an "expectation" that `all` will be called

double('the user') # same as the current `double`

To utilize stub and mock as in the example above, it would cause a massive breaking change. Totally worth it IMO.

/cc @dchelimsky

1.) https://github.com/btakita/rr

myronmarston commented 12 years ago

I thought about this a bit more, and I came up an idea that's similar, but avoids the massive breakage:

stubbing(User, :all) do
  the_users
end

mocking(User, :all)

The "-ing" forms aren't taken up by anything yet, and read nicely when used with the block form. It looks a little weird when you're just mocking it w/o an implementation, though.

The feedback on the recent syntax changes in rspec-expectations have been overwhelmingly positive, and I think the fact that we didn't break anything is a big part of that. I don't think massive breaking change is acceptable at this point, unless we decide to do it in a separate gem (e.g. rspec-better-mocks or whatever).

On a side note, if we can come up with a good solution here, we have the potential to get rspec in a place where the monkey patching it does is very, very minimal. Up to now, RSpec has added the following methods to every object in the system:

should and should_not have been dealt with w/ the expect syntax, and I just fixed rspec core in rspec/rspec-core#638 to no longer add describe to every object. The methods added by rspec-mocks are the last major source of monkey patching ever object, so if we can find a good solution here it would be great :).

dchelimsky commented 12 years ago

The "ing" idiom suggests to me "scoped within this block" e.g.

stubbing(User, :all => [User.new]) do
  # stub is scoped to this block
end
# that stub is no longer available

I actually have a never-used-lib that uses this syntax for that purpose.

Also, I'd prefer to have a syntax that aligns with the changes to rspec-expectations if we're going to do this. I'd be OK with:

stub(object, :method => value)
stub(object, :method) { lazy_value }
expect(object).to receive(:message)

I would not be OK with:

stub(object).method { lazy_value }
mock(object).method { lazy_value }

This syntax is no more terse than the others, but it is not intention revealing IMO.

myronmarston commented 12 years ago

@dchelimsky -- I like your thinking here, but won't stub(object, :method => value) be a breaking change? Currently the stub method within an example creates a new test double; here it stubs a method on the given object.

This wouldn't just be a breaking change; this would be an incredibly confusing breaking change. Removing a method, so that users start getting NoMethodErrors is one thing; taking an existing method, and completely changing it's semantics so that it does something entirely different will be far more difficult for users to deal with. They'll get confusing failures and won't have an easy way to scan their code to find the old uses of stub.

dchelimsky commented 12 years ago

@myronmarston STOP MAKING SENSE! :)

Yes, we'll need a new name, but that's the form I want to use: some_new_name(object, :method => value).

One option would be allow (comes from one of the java mock frameworks - can't recall which and google is not my friend today).

myronmarston commented 12 years ago

Maybe stub_method(object, :method => value)?

iain commented 12 years ago

Is it necessary to change stub at the same time? I know we want to fix the bug for stubbing, but should mocks wait for this?

@myronmarston For some reason I dislike the word method in there. I like the fact that mocks use the vocabulary of sending and receiving messages. Not sure on an alternative though.

dchelimsky commented 12 years ago

Any objections to allow? Then we'd have:

allow(object, :method => value)
allow(object, :method) { lazy_value }
expect(object).to receive(:message)
iain commented 12 years ago

Looks great!

myronmarston commented 12 years ago

Any objections to allow?

A couple concerns I have:

Here's another idea: respond_to:

respond_to(object, :method => value)
respond_to(object, :method) { lazy_value }
respond_to(object, :method).with(some_argument) { lazy_value }

When you stub a method, you're setting how an object responds to a particular message.

dchelimsky commented 12 years ago

Unfortunately, respond_to(object, :method).with(:foo) sounds like "respond to object.method by returning foo"

myronmarston commented 12 years ago

Yeah, I just realized that. API design is hard :(.

dchelimsky commented 12 years ago

More challenging when you have legacy to support :)

dnagir commented 12 years ago

Guys, how about this small idea...

Currently I write a lot of things like:

it "calculates thing weekly" do
  Calculator.should_receive(:annual_revenue).with(year: 5).and_return 520
  report.weekly_revenue.should == 10 # 520/52
end

The should_receive syntax is just a bit harder to read and type than what my eye & fingers want to:

it "calculates thing weekly" do
  Calculator.should_receive.annual_revenue(year: 5) { 520 }
  report.weekly_revenue.should == 10 # 520/52
end

Please consider this syntax or similar if it is something you think aligns with RSpec philosophy.

dchelimsky commented 12 years ago

@dnagir that's an interesting idea, but chaining the target method onto should_receive is too different from everything else we do in RSpec IMO. My guess is that what you're looking for is something like RR's syntax, which lets you use the same syntax for the method once you wrap the object: mock(Calculator).annual_revenue(year: 5) { 520 }. That's very nice in terms of mirroring part of the actual use, but I think it's also a bit disconnected due to the mix of wrapping the object. I don't have a better idea at this point, but I'm not in favor of should_receive.annual_revenue (though I am personally in favor of receiving annual revenue).

rosenfeld commented 12 years ago

@dchelimsky how about fake instead of stub?

rosenfeld commented 12 years ago

Or pretend?

dchelimsky commented 12 years ago

fake already has special meaning: http://xunitpatterns.com/Fake%20Object.html

pretend is interesting, but I don't like it initially - gut feeling. Need to think about it some more.

myronmarston commented 12 years ago

Another possibility: stub_message--it's a bit like my earlier suggestion (stub_method) but retains the vocabulary of sending and receiving messages.

dchelimsky commented 12 years ago

@myronmarston the message is what comes in, the method is how it responds.

myronmarston commented 12 years ago

on_receipt_of(object, message).with(arguments)?

Actually, I was just thinking about the original allow idea some more. allow would allow us (pardon the pun) to do:

expect(object).to receive(:message)
allow(object).to receive(:message)

I'm still concerned about the potential confusion of the language of "allowing" a real object to receive a message it can already receive anyway, but the symmetry here is really, really nice and I might actually like this one best after all since none of the other ideas have that symmetry and we haven't yet come up with one we're happy with.

dchelimsky commented 12 years ago

@myronmarston agreed so far, but open to other suggestions that maintain the same symmetry without the confusion about what allow means in the context of a real object.

mediafinger commented 12 years ago

I love the idea to use a consistent syntax for the mocks. And giving the stubs a different name, once the syntax changes, makes sense too. But allow does not sound that natural to me.

Maybe something like adjust or augment could work.

   expect(object).to receive(:message)
   augment(object).to receive(:message)

When staying close to the should_receive syntax, I would prefer something like 'simulate' or 'implements':

   Object.simulate(:method).with(param).and_return xyz
   Object.implements(:method).with(param).and_return xyz
kern commented 12 years ago

What about implement rather than allow? It makes sense for real objects.

implement(object, :method) { :bar }
myronmarston commented 12 years ago

I like the sense given by implement, but it doesn't read well as implement(object).to receive(:message), and I'd really like to have the symmetry of the same basic syntax.

augment (suggested by @mediafinger above) is pretty good, I think. It's certainly better for the case of a real object than allow, although I think for a pure mock object, allow still reads better.

So here's an idea: we can provide all 3 of these:

# sets a mock expectation
expect(object).to receive(:message)

# stubs the method
allow(object).to receive(:message)
augment(object).to receive(:message)

Basically, one of allow and augment would be an alias for the other, allowing the user to use whichever makes the most sense in their context (typically, augment for real objects and allow for pure mock objects).

dchelimsky commented 12 years ago

We're not augmenting it to receive a message, but to allow it to receive the message. The more I think of this I like allow, and I definitely don't want to add two names for a new feature. FWIW, I thought of "allow" because JMock uses "allowing", so it comes from the mock-library space (just not Ruby).

myronmarston commented 12 years ago

We're not augmenting it to receive a message, but to allow it to receive the message. The more I think of this I like allow, and I definitely don't want to add two names for a new feature. FWIW, I thought of "allow" because JMock uses "allowing", so it comes from the mock-library space (just not Ruby).

That's certainly true for a pure mock object (which is why I like allow a lot for that case), but IMO allow only makes sense if the object cannot already respond to the message--and with a real object that may not be the case. augment seems like a better word for real objects to me, but even it doesn't really have the right connotation.

dchelimsky commented 12 years ago

I see what you're saying about real objects, but "augment this object to receive message x" doesn't make any sense based on similar arguments: it can already receive x. I don't have a good answer, but here are some more bad answers to see if any spark better ideas:

tell(object).to respond_to(:message).with(value)
redef(object, :message) { value }
replace(object, :message) { value }

I hate all three :)

myronmarston commented 12 years ago

I've been thinking recently about the design benefits of pure mock objects. Partial mocks/stubs (i.e. on real objects) don't provide the same design and isolation benefits.

So...I think it makes sense to optimize rspec-mocks for the pure mock case while allowing its use on real objects. allow does this nicely: it reads really, really well for pure mock objects, and a bit funny for real objects. It's a subtle nudge that it's generally better to design your system and tests to work with pure mock objects.

All that is to say: I dislike your 3 suggestions as well, and I'm getting on board with adding expect/allow for mocks/stubs in spite of the funny wording for real objects.

kern commented 12 years ago

+1 for allow. I like that it's borrowed from jMock, so it isn't Ruby-specific vocabulary. I agree with @myronmarston that making partial mocking ugly isn't necessarily bad. It should be discouraged.

rubiii commented 12 years ago

the explanation given by @myronmarston makes sense to me! +1

jwilger commented 12 years ago

I'm also not a big fan of #allow, especially for stubbing methods on real objects (as opposed to pure stub/mock objects). How about #stub_on. I like the way that would read: stub_on( my_object, some_method: 'blah' ).

dchelimsky commented 12 years ago

I think I could live with stub_on as long as we also use expect_on so the methods align. WDYT?

myronmarston commented 12 years ago

stub_on and expect_on align nicely. But does it work with the rest of the fluent interface (e.g. with(some, args), exactly(3).times, etc? I'd like to preserve as much of that fluent interface as possible.

dchelimsky commented 12 years ago

Again w/ the sense. That won't work at all, and I'm at a bit of a loss for new ideas. So far, I think expect/allow is the best pairing. More ideas?

jwilger commented 12 years ago

Why does that not work at all?

stub_on( my_object, foo: 'bar' )

expect_on( my_object, :foo ).with( 'some', 'args' ).exactly( 3 ).times
# or
expect_on( my_object ).message( :foo ).with( 'some', 'args' ).exactly( 3 ).times

(Not sure #message in the last line would be the best method name, but it illustrates the point.)

jwilger commented 12 years ago

Another possibility: separate the injection of the methods from the definition of the stubs.

allow_stubbing_on( my_object )

my_object.stubs( foo: 'bar' )

Or, what about #override instead of (or in addition to) #allow.

(Just brainstorming here.)

dchelimsky commented 12 years ago

How about something like this:

on(obj).expect(message).with(args).and_return(val)
on(obj).stub(message).and_return(val)

The new syntax is only on and expect. Everything else stays the same. Is on too likely to general (i.e. likely to create conflicts)?

dchelimsky commented 12 years ago

@jwilger re my_object.stubs( foo: 'bar' ), we've already got three ways to define return values, so I don't want to add another as part of this.

obj.stub(m => v)
obj.stub(m) { v }
obj.stub(m).and_return(v)

Nice thing about on is all that just changes to:

on(obj).stub(m => v)
on(obj).stub(m) { v }
on(obj).stub(m).and_return(v)

I'm open to a different word, but I'm quickly growing attached to the idea of adding a single wrapper function to allow us not to have to add methods to all objects. This is what flexmock and RR both do (though their syntax is a bit different) and I'm a big fan.

jwilger commented 12 years ago

I agree that the idea of having a single wrapper method is the way to go. I'm not a fan of #on, as it doesn't reveal much about what it actually does.

re: my_object.stub(foo: 'bar'); Unless it's about the 's' that I accidentally added to the end of the #stub (I've been working a lot on a project that uses Mocha), I'm not sure what you mean, since that's equivalent to obj.stub(m => v).

dchelimsky commented 12 years ago

I missed the trailing colon. I thought you were saying obj.stub(m, v). Never mind :)

jwilger commented 12 years ago

Some other possibilities:

mask(obj).stub(m => v)
mask(obj).expect(m).and_return(v)

screen(obj).stub(m => v)
screen(obj).expect(m).and_return(v)

disguise(obj).stub(m => v)
disguise(obj).expect(m).and_return(v)

relieve(obj).stub(m => v)
relieve(obj).expect(m).and_return(v)

take_over(obj).stub(m => v)
take_over(obj).expect(m).and_return(v)

simulate(obj).stub(m => v)
simulate(obj).expect(m).and_return(v)

mimic(obj).stub(m => v)
mimic(obj).expect(m).and_return(v)

affect(obj).stub(m => v)
affect(obj).expect(m).and_return(v)
jwilger commented 12 years ago

Out of those, I think I like #mask the best. When adding a mock or stub method to a real object, you are, in effect, applying a mask. The method name is short, easy to spell, and reveals the intention pretty well.

myronmarston commented 12 years ago

A huge +1 to @dchelimsky's idea of on. I like how it reads a lot. I like that it allows us to preserve the rest of the current fluent interface. I like that it's a very short word (so it doesn't make the code much longer--in fact, on(foo).expect is shorter than foo.should_receive. I like that it works for mocks and stubs.

One side comment: this isn't really fulfilling @iain's original request of using the new rspec-expectations syntax for mocks, but we've seen how difficult it is to come up with a way of using that syntax for mocks and stubs, and I'm more interested in solving the issue of adding stub and should_receive to every object (with the proxy/delegate issues that creates) than in mirroring the syntax of rspec-expectations.

Is on too likely to general (i.e. likely to create conflicts)?

I think it's actually so general that it's unlikely to create conflicts :). That sounds like an oxymoron, but on is a word on the level of the or an or with--it's a grammar word (a preposition in this case) that on its own doesn't really have any meaning, without the surrounding context of other words. I think it only makes sense to be used as part of a fluent interface. I can't imagine anyone having a stand-alone spec helper method called on--it reveals no intention whatsoever. Given that it makes little sense outside of a fluent interface, I find it unlikely that end users will have on defined in their example groups.

That said, if we want to be conservative, we could play it safe and wait to introduce this until 3.0, or introduce it in a future 2.x release, but with the syntax options (similar to rspec-expectations') defaulted to only the current syntax. However, if we do this, I'd like to get it out in a 2.x release with the options defaulting to both syntaxes being available, so the rest of the 2.x can be a transitionary period to the time when we default the old syntax off (potentially 3.0, but that's obviously very, very open to discussion and change).

iain commented 12 years ago

I like on too, but it moves away from a more unified syntax. Now this has never really been the case: it's should_receive and not should receive.

For the same reason, I don't like that the new expect syntax doesn't use blocks, as in: it would be neat if expect { obj }.to eq 5 was also supported. But that is besides the point right now.

We've been coming up with alternative syntaxes for quite a while now, but I fear mocking and stubbing is drifting too far apart from the rest of the RSpec syntax.

We could just deprecate stub in RSpec 2, and change the meaning in RSpec 3. That way we can use expect(object).to receive(method) for mocks and stub(object, :method => value). We got 2 other aliases for stub anyway. Too radical?

myronmarston commented 12 years ago

I like on too, but it moves away from a more unified syntax.

FWIW, I've been thinking about the unified syntax possibilites of expect(obj).to receive a bit today as this discussion goes on, and while I like that it is unified with rspec-expectations, I've been thinking it's probably a good thing to not re-use the same syntax for something entirely different. expect(obj).to matcher fails the example (or not) based on the current state of obj. expect(obj).to receive looks the same but would do something entirely different. It's not confusing for those of us who have been participating in this conversation, but in general, similar-looking constructs should behave similarly and different-looking constructs should behave differently. It could lead to a great deal of confusion on the part of new RSpec users.

For the same reason, I don't like that the new expect syntax doesn't use blocks, as in: it would be neat if expect { obj }.to eq 5 was also supported. But that is besides the point right now.

It could probably theoretically be supported, but I'm not sure what the advantage is. Plus, the new expect syntax does support blocks, for when you are expecting something will happen as the result of running a bit of code (e.g. change, raise_error, throw_symbol, yield_control, etc). I think it would be confusing to support a block for value expectations when a block is already used for block expectations. That's a different discussion, though :).

We could just deprecate stub in RSpec 2, and change the meaning in RSpec 3. That way we can use expect(object).to receive(method) for mocks and stub(object, :method => value). We got 2 other aliases for stub anyway. Too radical?

Even though I'm thinking that unity of syntax w/ rspec-expectations isn't desired here, I do think unity of syntax of mocks and stubs are, particularly when the fluent interface comes into play. It's not clear to me how the stub(object, :method => value) syntax would support that. And changing the meaning of stub would lead to greater pain for those upgrading than introducing a new method (like on) will.

rosenfeld commented 12 years ago

I'm definitely +1 for David's on suggestion

myronmarston commented 12 years ago

Some further thinking of mine regarding on:

on(obj) do |o|
  o.mock(:foo) { "foo value" }
  o.stub(:bar) { "bar value" }
end

These are all just current thoughts of mine...I'm not wedded to any of this.

dchelimsky commented 12 years ago

On the fence re: "mock". Agree it is concise, and most people will know what it means. The problem is that 10 people will answer "what does mock mean" differently.

Regardless of the word we end up w/, I like on(obj) {|o| ...} and think it should apply to real objects and doubles (what you're referring to as "pure mocks") alike.

re: unstub, what do you think about changing that to restore in the context of on (aliased to unstub for compatibilty): on(obj).restore(:previously_stubbed_method)

re: negative expectations, expect actually works quite nicely: on(obj).expect(:message).never

More thoughts?