Testing with has_many associations

Hi I’m trying to be good and practice full BDD on my current project,
and don’t want to abandon it as I have previously (expediency triumphed
unfortunately). So expect me to be making frequent ‘noob’ style posts…

My current issue is with testing assignation across a has_many
relationship. I’m aware I shouldn’t be testing the functionality of
Rails, but this is behaviour of my code.

cart_spec.rb:

describe Cart do
before(:each) do
@product = mock “Trousers”
@product.stub!(:class).and_return(“Product”)
@product.stub!(:name).and_return(“Brown Trousers”)
@product.stub!(:price).and_return(23.99)
@cart = Cart.new
end

it “should have 1 item after adding a Product” do
@cart.add_product(@product)
@cart.items.should have(1).item
end

it “should have 1 item but with quantity 2 after adding the same
product twice” do
@cart.add_product(@product)
@cart.add_product(@product)
@cart.items.should have(1).item
end
end

cart.rb:

class Cart < ActiveRecord::Base

has_many :items, :class_name => “CartItem”, :dependent => :destroy

def add_product(product)
current = self.items.find_by_name(product.name)
if current
current.increment_quantity
else
self.items << CartItem.new_from_product(product)
end
end

end

fails with:

‘Cart should have 1 item but with quantity 2 after adding the same
product twice’ FAILED expected 1 item, got 2


Can anyone explain to me what I’m missing?

On 9 Apr 2008, at 06:25, Andy C. wrote:

current = self.items.find_by_name(product.name)

You’re finding by the CartItem name and passing the product name - it
looks like that’s drawing a blank match when you’re expecting it not to.

How about a cart_item_spec.rb:

describe CartItem do
it “should be created successfully from a new product” do
CartItem.new_from_product(mock_model(Product, :name => “name” ))
CartItem.find_by_name(“name”).should_not be_nil
end
end

I’m guessing this spec will fail with your current code as I don’t
think CartItem::new_from_product is working.

I’d also probably break down the specs differently and wrap the call
to items and make the specs more contained:

class Cart
def self.find_items_by_name(name)
items.find_by_name(name)
end
end

…and mock this method when testing add_product:

it “should add the product when it doesn’t already exist in the cart” do
@cart.should_receive(:find_items_by_name).with(“name”).and
return(nil)
@cart.add_product(@product)
@cart.items.should have(1).item
end

it “should increment quantity when it does find a product” do
@cart.should_receive(:find_items_by_name).with(“name”).and
return(@product)
@cart.add_product(@product)
@cart.items.should have(1).item
end

HTH,
Chris

Chris Parsons wrote:

I’m guessing this spec will fail with your current code as I don’t
think CartItem::new_from_product is working.

Wow. I’ve never had code I hadn’t posted correctly debugged before. You
were absolutely right, thanks! Seems my testing approach in that spec
was somehow faking out the all the tests I made.

I’d also probably break down the specs differently and wrap the call
to items and make the specs more contained:

I think this is where I’m going wrong, I keep letting my tests get too
big. Even when I think they are small. Another good rule I’ve picked up
from this is to always try and use less than two dots to abstract the
models…

Big help thanks Chris.

Chris Parsons wrote:

it “should increment quantity when it does find a product” do
@cart.should_receive(:find_items_by_name).with(“name”).and
return(@product)
@cart.add_product(@product)
@cart.items.should have(1).item
end

My final solution for this was to write…

describe Cart do
before(:each) do
@product1 = mock_model Product, :name => “Brown Trousers”, :price =>
23.99
@product2 = mock_model Product, :name => “Yellow Shirt”, :price =>
15.74
@cart_item1 = mock_model CartItem, :name => “Brown Trousers”, :price
=> 23.99, :quantity => 1
@cart = Cart.new
end

it “should increment quantity when it does find a product” do
@cart_item1.should_receive(:increment_quantity).once.with(:no_args).and_return(2)
@cart.should_receive(:find_item_by_name).twice.with(“Brown
Trousers”).and_return(nil, @cart_item1)
@cart.add_product(@product1)
@cart.add_product(@product1)
@cart.items.should have(1).item
end
end

Which seems to work. I’m right?

On 9 Apr 2008, at 12:22, Andy C. wrote:

end

Which seems to work. I’m right?

Looks good. Don’t forget that with that extra mocking your coverage
has dropped on CartItem so you’ll need to ensure that it’s correctly
tested also.

Cheers
Chris