¿Existe una alternativa menos intrusiva a "should_receive" de Rspec?

In writing Rspec tests, I'm often frustrated with should_receive. I'd like to know if there's a less intrusive alternative.

Por ejemplo:

describe "making a cake" do
  it "should use some other methods" do
    @baker.should_receive(:make_batter)
    @baker.make_cake
  end
end

La llamada a la should_receive is a good description, but it breaks my code, because should_receive works by masking the original method, and make_cake can't proceed unless make_batter actually returns some batter. So I change it to this:

@baker.should_receive(:make_batter).and_return(@batter)

This is ugly because:

  • It miradas like I'm testing that make_batter regresa correctamente @batter, but I'm realmente forcing the fake version of make_batter to return that.
  • It forces me to separately set up @batter
  • If make_batter has any important side effects (which could be a code smell, I suppose) I have to make those happen, too.

me gustaría que should_receive(:make_batter) would verify the method call and pass it on to the original method. If I wanted to stub its behavior for better isolation testing, I would do so explicitly: @baker.stub(:make_batter).and_return(@batter).

¿Hay alguna manera de hacer algo como should_receive without preventing the original method call? Is my problem a symptom of bad design?

preguntado el 28 de agosto de 12 a las 13:08

3 Respuestas

It looks like the nicer API to delegate to the original method that Myron Marston alluded to has actually been added in rspec-mocks v2.12.0

So now you can simply do this any time you "want to set a message expecation without interfering with how the object responds to the message":

@baker.should_receive(:make_batter).and_call_original

Thanks for adding this, Myron.

Respondido 04 Feb 13, 19:02

Y gracias for revisiting this question with an updated answer. - nathan largo

Tu puedes tener should_receive run the original method like so:

@baker.should_receive(:make_batter, &@baker.method(:make_batter))

Ambos should_receive y stub support passing a block implementation (that is eval'd when the method is called). &@baker.method(:make_batter) gets a handle to the original method object, and passes it along as the block implementation.

FWIW, we'd like to provide a nicer API to delegate to the original method (see este problema), but it's difficult to add that functionality without breaking backwards compatibility.

Respondido 28 ago 12, 15:08

Neat! I didn't realize should_receive could take a block. Also good to see that I'm not the only one who wants this. - nathan largo

Alternate (possibly unworkable) idea: what if, in my example, the test simply bailed from bake_cake una vez make_batter had been received? I shouldn't be testing more than one thing at a time anyway, right? - nathan largo

I created a little helper method for this: def should_receive_and_proceed(object, method); object.should_receive(method, &object.method(method)); end - transatlántico

Given it is 2016 now, I think @Tyler Rick's answer above is more appropriate: stackoverflow.com/a/14693779/224707 - Nick

You're having this problem with should_receive because you're testing implementation details of the make_cake method. When writing tests you should only focus on behavior and not on a sequence of internal method calls. Otherwise refactoring your code later would result in a huge refactoring of all your tests as well.

Mocks and Stubs come in handy when you want to test your classes in isolation. When writing unit tests your test subject should be isolated from any other object. Both act as a stand-in when you're working with multiple objects at once. In this case you can use should_receive to ensure that your test subject correctly delegates some task to another object by calling a method.

Respondido 28 ago 12, 14:08

Your overall point about not testing implementation details feels correct, but it seems that's the only use case for should_receive, right? I'm still unclear how should_receive(:foo) can be used at all without and_return if foo is supposed to return something. - nathan largo

Also, probably my lousy example obscures the issue. Suppose I've written a wrapper method for some library. I want to test that my wrapper calls the underlying library method with the correct arguments, and my method also needs to parse what it gets back before returning. I think that puts me back to some_library.should_receive(:some_method).with('foo', 'bar').and_return('thingy'), right? In this case, I'm not testing an implementation detail, but the purpose of the wrapper method. - nathan largo

In this case you of course want to return something. But in order to keep up the isolation I'd fake the return value from the library. (A good example would be a library that wraps a web service. I wouldn't want my tests to call out for the web service every time I run them and I don't want to rely on all the parts that could go wrong during a call to it) That's of course something I'd only do in unit tests. When writing integration tests I'd want all levels of the implementation the be triggered. - Benedikt Deicke

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.