1

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

2
  • In the tests are orders ever passed in to the User initialize method ? seems like the initialize should just set it to an empty array and not pass the value in? Commented Jun 19, 2014 at 22:00
  • I've added an example of the test flow! It looks like orders are only initliased by the user class Commented Jun 19, 2014 at 23:09

1 Answer 1

5

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.

Sign up to request clarification or add additional context in comments.

6 Comments

It's always something insanely simple. Thank you so much. I should mention that this a coding exercise so I'm not able to change the unit spec! But thank you for such a detailed answer. Did you mean not passing in order instead of voucher? I'm trying to think of a good way of not creating the array in the initializer but I'm not to sure. If I was thinking in another language I'd probably just create array empty instead of with a nil value, or make sure the nil value is skipped etc etc. Could I possibly bother you for one more poke in the right direction?
I guess what I'm trying to say is what would be the best way to avoid this initialisation issue? I imagine, creating the array outside of the constructor would be a start but not sure where...
Passing orders to the initializer is perfectly fine. You want to assign it to the instance variable doing @orders = orders instead of @orders = [orders], though. The latter will wrap the array of orders in another array, which isn't what you want.
I thought the @orders array contained an instance of a single order object? This is exactly what I want as the customer can have multiple orders and the test would loop through these
Certainly, if you pass an instance of Order to the orders parameter when creating your User, @orders will be an Array containing that one Order instance. However, the parameter name orders being plural and the default value being [] indicates that the parameter should be given an array. You then put that array inside an array and store the value in @orders, which might or might not be what you want. But we're probably getting outside the scope of the question here.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.