I am writing a unit test for one of my service objects. In this particular case, I needed to use transactions to ensure data integrity. Thus, I have a simple code like so:
class CreateUser
  def save
    user_klass.db.transaction do
      user = user_klass.create(name: name, email: email)
      another_model_klass.find_or_create(user_id: user.id, foo: 'foo')
    end
  end
end
I am using Sequel as my ORM. However, the important point of this question is actually how to test this code. I have been successfully using mocks and stubs but this is the first time I have to stub out something with a block involved.
At first I have a naive spec like so:
    describe CreateUser do
      describe "#save" do
        let(:user)                { instance_double("User", id: 1) }
        let(:user_klass)          { double("Class:User", create: user) }
        let(:another_model_klass) { double("Class:AnotherModel") }
        let(:name)                { 'Test User' }
        let(:email)               { '[email protected]' }
        let(:foo)                 { 'foo' }
        let(:params)              { { name: name, email: email, foo: foo } }
        let!(:form)               { CreateUser.new(params, user_klass, another_model_klass) }
        before do
          allow(another_model_klass).to receive(:find_or_create)
        end
        it "sends create message to the user_klass" do
          expect(user_klass).to receive(:create).with({ name: name, email: email}).and_return(user)
          form.save
        end
        it "sends find_or_create message to another_model_klass" do
          expect(another_model_klass).to receive(:find_or_create).with(user_id: user.id, foo: foo)
          form.save
        end
      end
    end
This gives out an error:
Double "Class:User" received unexpected message :db with (no args)
But if I add the following:
allow(user_klass).to receive_message_chain(:db, :transaction)
It would stub out the contents of the transaction block and it would still fail.
How do set expectations on my spec where:
- expect transaction to be used
 - expect the create message to be sent to user_klass
 - expect the find_or_create message to another_model_klass
 
                        
Take a look at spies https://github.com/rspec/rspec-mocks#test-spies, you might be able to drop them in your doubles. :-)