I know I'm replying to an old thread, but it seemed relevant.

Suppose we want to unit test in pure isolation a method that passes a block 
to a collaborator.  For example:

    def baz
      CollaboratorA.foo do
        CollaboratorB.bar
      end
    end

It's important not only that `baz` calls `CollaboratorB.bar`, but that it 
does it inside the block. Myron's gist 
<https://gist.github.com/myronmarston/e79cbff12ce51b54814b> from a year ago 
answers this question well.  However, what if we take this a step further, 
and say that we want to call `CollaboratorB.bar` once inside the block and 
once outside the block like this:

    def baz
      CollaboratorA.foo do
        CollaboratorB.bar
      end
      CollaboratorB.bar
    end

Furthermore, let's suppose we want to assemble the return values from these 
two `bar` calls.  This isn't a Rails specific question, but I'll use 
`ActiveRecord::Base.unscoped` (assuming a default_scope is set) as a 
real-world example that I hope will be easily understood:

    def fetch_products
      visible_products = Product.all
      all_products = Product.unscoped do
        Product.all
      end
      { visible_products: visible_products, all_products: all_products }
    end

I know that an isolated test for this will be tightly coupled to the 
implementation, but assuming that's what we want to do, I imagined a new a 
new `inside` and `outside` API for rspec-mocks, and this is how I would 
test `fetch_products`:

    it "fetches products" do
      all_products = double("all products")
      visible_products = double("visible products")

      unscoped = allow(Product).to receive(:unscoped).and_yield
      allow(Product).to 
receive(:all).inside(unscoped).and_return(all_products)
      allow(Product).to 
receive(:all).outside(unscoped).and_return(visible_products)

      expect(fetch_products).to eq({
        visible_products: visible_products,
        all_products: all_products
      })

    end

If you didn't have return values to compare against, you could also write 
something like:

    expect(Product).to have_received(:all).inside(unscoped).once
    expect(Product).to have_received(:all).outside(unscoped).once


So I have a two questions:
  
 1. Is there a straightforward way to write this spec using the current API?
 2. If not, would you be open to a PR that adds the `inside` and `outside` 
API, or open to discussing an alternative API that would make this kind of 
spec feasible?

I have a working prototype locally that enhances `@messages_received` to 
track what existing message expectations are running an implementation when 
a subsequent message is called, which can then be compared to to 
constraints.

Thank you,

Nathan
 
On Thursday, March 5, 2015 at 9:16:31 PM UTC-7, Myron Marston wrote:
>
> On Thursday, March 5, 2015 at 7:06:58 PM UTC-8, Joe Van Dyk wrote:
>>
>> Say I have the following method:
>>
>>       def run
>>         transaction do
>>           mark_non_eligible
>>           make_invoice_batch
>>           send_batch_email
>>         end
>>       end
>>
>> How can I test that with rspec's mocks?
>>
>> Joe
>>
>
> There's a rarely-used feature of `and_yield` that can help you with this. 
>  If you pass a block to `and_yield`, RSpec will pass your block an object 
> that it will use to instance_eval the `transaction` block, allowing you to 
> set message expectations on it.  I put together an example gist:
>
> https://gist.github.com/myronmarston/e79cbff12ce51b54814b
>
> This works, but bear in mind there are a number of code smells here 
> (mocking the object-under test, making a test that simply mirrors the 
> implementation, etc).  IMO, you'd be better off testing this with an 
> integrated test that hits the DB.  You could, for example, stub whatever 
> collaborator `send_batch_email` delegates to so that it fails, and check 
> that the database updates performed by `mark_non_eligible` and 
> `make_invoice_batch` are rolled back.
>
> HTH,
> Myron
>

-- 
You received this message because you are subscribed to the Google Groups 
"rspec" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/rspec/c9704cee-9a7c-404f-837f-514d90c89dcc%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Reply via email to