Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RSpec 3: How to stub methods and constants from code we have yet to build/add?

Tags:

ruby

rspec

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."

like image 316
La-comadreja Avatar asked Mar 17 '15 23:03

La-comadreja


1 Answers

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
like image 101
cbliard Avatar answered Sep 23 '22 10:09

cbliard