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. :-)