We are refactoring a Ruby application called DataSourceIntegrations from a gem we've built called DBQuery. I am migrating some of the DBQuery code into DataSourceIntegrations. The section I'm building depends on DBQuery, which will be added in a separate step.
Meanwhile, I need to write RSpec tests to verify that the DBQuery code is being called correctly, all without DBQuery.
What I have is:
Code—
Gem code—
module DBQuery
class Query
MAX = 1000
def retrieve_users
# Returns an array of user IDs
end
end
end
Application code—
module Integration
def initialize
@query = DBQuery::Query.new
end
end
module Integration
class StackOverflowIntegration
include Integration
def query
users = []
while (users < DBQuery::Query::MAX) do
# Creates a users buffer
users.push @query.retrieve_users(users_buffer)
end
end
end
end
Tests—
describe Integration::StackOverflowIntegration do
let(:db_query) { double('DBQuery::Query') }
before do
stub_const('DBQuery::Query::MAX', 1000)
allow(db_query).to receive(:new).and_return(db_query)
allow(db_query).to receive(:retrieve_users).and_return([1000, 1001, 1002])
end
it 'queries without error' do
expect { StackOverflowIntegration.new.query }.to_not raise_error
end
end
I can't figure out how to stub in a way that doesn't require DBQuery. My error is:
NoMethodError:
undefined method `new' for #<Module:0x007fa7ce561968>
I don't know why DBQuery::Query is being represented as a module, or how to get it to recognize "new."
As I understand, you want to make expectations on DBQuery::Query
without defining it in your code. rspec-mocks can stub an undefined constant, like you did for DBQuery::Query::MAX
. To stub DBQuery::Query
completely, create a class double first and stub a const for it in your test:
db_query__query_class = class_double('DBQuery::Query')
stub_const('DBQuery::Query', db_query__query_class)
This way, DBQuery::Query
in your code will return the query_class
double. Then you can define some behavior with it:
query_instance = instance_double('DBQuery::Query')
allow(db_query__query_class).to receive(:new).and_return(query_instance)
allow(db_query).to receive(:retrieve_users).and_return([1000, 1001, 1002])
You still have to stub nested constants like DBQuery::Query::MAX
stub_const('DBQuery::Query::MAX', 1000)
About style, I prefer putting stubs and allows in let
/let!
statements like this:
describe Integration::StackOverflowIntegration do
let!(:db_query__query_class) do
class_double('DBQuery::Query').tap do |double|
stub_const('DBQuery::Query', double)
stub_const('DBQuery::Query::MAX', 1000)
allow(double).to receive(:new).and_return(query_instance)
end
end
let(:query_instance) do
instance_double('DBQuery::Query').tap do |double|
allow(double).to receive(:retrieve_users).and_return([1000, 10001, 1002])
end
end
end
Also I like to put returned values in their own let
so I can change them easily. Here is a full working (and dummy) example:
RSpec.configure do |c|
c.around(:context, :protect_with_timeout) do |example|
Timeout::timeout(2) {
example.run
}
end
end
describe Integration::StackOverflowIntegration do
let!(:db_query__query_class) do
class_double('DBQuery::Query').tap do |double|
stub_const('DBQuery::Query', double)
stub_const('DBQuery::Query::MAX', max_queries)
allow(double).to receive(:new).and_return(query_instance)
end
end
let(:query_instance) do
instance_double('DBQuery::Query').tap do |double|
allow(double).to receive(:retrieve_users).and_return(retrieved_users)
end
end
let(:max_queries) { 1000 }
let(:retrieved_users) { [1000, 1001, 1002] }
describe '#query' do
subject(:stack_overflow_query) { Integration::StackOverflowIntegration.new.query }
it 'queries without error in nominal case' do
expect { stack_overflow_query }.to_not raise_error
end
context 'with 0 users returned' do
let(:retrieved_users) { [] }
it 'does not loop forever', :protect_with_timeout do
pending('not implemented yet...')
stack_overflow_query # will timeout
end
end
context 'with 10 users returned' do
let(:retrieved_users) { [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }
it 'calls #retrieve_users 100 times' do
stack_overflow_query
expect(query_instance).to have_received(:retrieve_users).exactly(100).times
end
end
context 'with DBQuery::Query::MAX set to 0' do
let(:max_queries) { 0 }
it 'does not call #retrieve_users at all' do
stack_overflow_query
expect(query_instance).not_to have_received(:retrieve_users)
end
end
end
end
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With