user3750194
user3750194

Reputation: 407

Undefined Method in rspec testing

I'm running an integration test in rspec and the test keeps throwing up an undefined method on billed_for:

"undefined method billed_for nil:NilClass"

require 'user'

describe "Integration" do
  let(:user) { User.new(voucher) }

  context 'no voucher' do
    let(:voucher) { nil }

    it 'should bill default price all the time' do
      user.bill
      expect(user.orders[0].billed_for).to eql 6.95
    end
  end
end

I have a very small user class so far

require 'order'
require 'voucher'

class User
  attr_accessor :voucher, :orders

  def initialize(orders = [], voucher = nil)
    @voucher = voucher
    @orders = [orders]
  end

  def bill
    new_order = Order.new(self)
    @orders << new_order
  end
end

and an equally small order class:

class Order
  DEFAULT_PRICE = 6.95

  attr_accessor :user

  def initialize(user)
    @user = user
  end

  def billed_for
    price = DEFAULT_PRICE
    user.orders.each do |order|
        price - order.billed_for
    end
    price
  end
end

What's confusing me most is this line

user.orders[0].billed_for

when I think it through a new user class is set up this let, I then access the orders hash in user hash and then I'm accessing the billed_for method within the order class.

When I've googled this issue it's pointed towards using the self keyword that isn't working.

If someone could point me in the right direction it'd be great

EDIT:

Jakob S kindly pointed at that my test was failing because of nil entries in my array.

A quick an dirty fix for this was just to run the compact function to remove the nil entry.

Always open to better solutions of course.

EDIT 2:

let(:user) { User.new(voucher) }

context 'no voucher' do
  let(:voucher) { nil }

  it 'should bill default price all the time' do
      user.bill
      expect(user.orders[0].billed_for).to eql 6.95
      ... ...
  end
end

context 'vouchers' do
  describe 'default vouchers' do
    let(:voucher) { Voucher.create(:default, credit: 15) }

    it 'should not bill user if has a remaining credit' do
      user.bill
      expect(user.orders[0].billed_for).to eql 0.0
      ... ...
    end
  end

Thanks for the help so far. I've also opened an additional thread as I had a few other similar questions

Accessing variables of other classes

Upvotes: 1

Views: 3205

Answers (1)

Jakob S
Jakob S

Reputation: 20135

When you instantiate your user, you use

let(:user) { User.new(voucher) }

voucher is defined as nil in

let(:voucher) { nil }

In other words you instantiate your user variable with User.new(nil).

Your User constructor has the signature

def initialize(orders = [], voucher = nil)

so by doing User.new(nil) you're setting the orders argument to nil (voucher is also nil, but that's by default). Your constructor then goes ahead and creates an instance variable, @orders that it sets to [orders] - which in this case is the same as [nil].

Your test then goes ahead and adds a new order to the @orders Array, which is fine, and that leaves your @orders array containing [nil, instance_of(Order)].

Finally, the test tries to send the billed_for method to the first elements in the orders array: user.orders[0].billed_for. The orders array contains [nil, instance_of(Order)], the first element of that is nil, thus you're actually calling

nil.billed_for

in your spec, which results in the error you're seeing.

I think you might get a bit closer to what you're looking for by not passing the voucher to the orders argument when instantiating the User. Also your test might want to check the last element, ie user.orders.last rather than user.orders[0]. And I suspect you might stumble across a few more improvements as you go along.

Upvotes: 5

Related Questions