Nathaniel Wroblewski in the Machine Age

Well-Tested Ruby - Stubs

Let’s build off our previous discussion of cancelling an order. Currently, #cancel will update the status attribute of our order object to 'cancelled'. Here is the code we ended with:

app/models/order.rb

class Order < ActiveRecord::Base

  def cancel
    update_attributes(state: 'cancelled')
  end
end

spec/models/order_spec.rb

require 'spec_helper'

describe Order, '#cancel' do
  it 'changes the order status to cancelled' do
    order = Order.new(status: 'complete')

    order.cancel

    expect(order.status).to eq 'cancelled'
  end
end

Before we get started, let’s replace our constructor with a factory; we’ll use the popular factory_girl library.

Now our code looks like this:

spec/models/order_spec.rb

require 'spec_helper'

describe Order, '#cancel' do
  it 'changes the order status to cancelled' do
    order = build(:order, status: 'complete') # factory

    order.cancel

    expect(order.status).to eq 'cancelled'
  end
end

And we have a factory file where we define our factories:

spec/factories.rb

FactoryGirl.define do
  factory :user
end

Factory girl is simple, it allows you to define a factory which is basically a model, assign default attributes to model objects you plan on using in your tests, and gives you a simple syntax for creating objects.

Rails Factory Girl
Order.new(status: :complete) build(:order, status: :complete)
Order.create(status: :complete) create(:order, status: :complete)

Now, onto business.

Let’s introduce another model: inventory units. Now, we’re tasked with #cancel having to restock the inventory units associated with our order. For simplicity’s sake, let’s assume an order can only be for one type of item, and that our inventory units represent the items being purchased, i.e. an item being purchased does not consist of components that are represented as inventory units. Bascially, an order consists of one or more of the same inventory unit. Let’s start with a unit test on the inventory unit model.

spec/factories.rb

FactoryGirl.define do
  factory :user
  factory :inventory_unit, class: Inventory::Unit
end

spec/models/inventory/unit_spec.rb

require 'spec_helper'

describe Inventory::Unit, '#restock' do
  it 'increments the inventory quantity on hand' do
    unit = build(:inventory_unit, on_hand: 10)

    unit.restock(20)

    expect(unit.on_hand).to eq 30
  end
end

The tests will drive the code:

app/models/inventory/unit.rb

class Inventory::Unit < ActiveRecord::Base
  def restock(quantity)
    update_attributes(on_hand: on_hand + quantity)
  end
end

There may be some setup involved if you want to namespace the inventory unit, and have it play nice with active record, but that’s for another time. Let’s also check that our quantity is always a positive number.

spec/models/inventory/unit_spec.rb

require 'spec_helper'

describe Inventory::Unit, '#restock' do
  it 'increments the inventory quantity on hand' do
    unit = build(:inventory_unit, on_hand: 10)

    unit.restock(20)

    expect(unit.on_hand).to eq 30
  end

  it 'will not deduct inventory' do # check for positive input
    unit = build(:inventory_unit, on_hand: 10)

    unit.restock(-5)

    expect(unit.on_hand).to eq 10
  end
end

Our resulting code:

app/models/inventory/unit.rb

class Inventory::Unit < ActiveRecord::Base
  def restock(quantity)
    update_attributes(on_hand: on_hand + quantity) if quantity > 0
  end
end

Great, now our unit tests are in order, but if our order model is going to communicate to our inventory unit model, they should probably be related. Because orders can only be for one inventory unit in this example, and inventory units will therefore have many orders/purchases, we could have a situation like this:

app/models/inventory/unit.rb

class Inventory::Unit < ActiveRecord::Base
  has_many :orders, class_name: '::Order'
end

app/models/order.rb

class Order < ActiveRecord::Base
  belongs_to :inventory_unit, class_name: 'Inventory::Unit'
end

But, do we really need to load the relation both ways? Not really. Right now, only our order model needs to communicate to our inventory unit. So, let’s not load both associations, and let’s only make the association one way. To test it, we can use the popular shoulda-matchers from thoughtbot.

spec/models/order_spec.rb

require 'spec_helper'

# here '{ }' replaces 'do end', the magic 'subject' replaces
# 'Order.new', and the it 'description' is ommitted
describe Order, 'associations' do
  it { expect(subject).to belong_to(:inventory_units) }
end

describe Order, '#cancel' do
  it 'changes the order status to cancelled' do
    order = build(:order, status: 'complete')

    order.cancel

    expect(order.status).to eq 'cancelled'
  end
end

And the code:

app/models/order.rb

class Order < ActiveRecord::Base
  belongs_to :inventory_unit, class_name: 'Inventory::Unit'

  def cancel
    update_attributes(state: 'cancelled')
  end
end

Now, we can restock the inventory when #cancel is called. And here is where we use a stub. A stub is used to control the effects of a method call. If we stub a method #bark on a Dog object, we can override the real effects of #bark and return anything we want. We can also tell if our Dog object ever had #bark called on it. For example:

dog = build(:dog)
dog.stub(:bark) # stub bark so nothing happens when it's called
dog.stub(:bark).and_return('quack') # override bark
dog.stub(:bark).and_yield('moo') # yield to a block

dog.bark

expect(dog).to have_received(:bark) # => true

With respect to our order model, we can stub the restocking communication sent to the inventory unit model like so:

spec/models/order_spec.rb

require 'spec_helper'

describe Order, 'associations' do
  it { expect(subject).to belong_to(:inventory_units) }
end

describe Order, '#cancel' do
  it 'changes the order status to cancelled' do
    order = build(:order, status: 'complete')

    order.cancel

    expect(order.status).to eq 'cancelled'
  end

  it 'restocks the inventory associated with the order' do
    order = build(:order, quantity: 5)
    order.inventory_unit.build
    order.inventory_unit.stub(:restock) # le stub

    order.cancel

    expect(order.inventory_unit).to have_received(:restock).with(5)
  end
end

Notice how the preparation section of our unit test is getting bigger? That’s no good. One thing we can do to refactor is pull out shared factories and put them in a let block.

The let block lets us define a variable and set its value:

let(:order) { build(:order, status: :complete) }
# order = Order.new(status: :complete)
let(:user) { create(:user, email: 'boom@pop.com'}
# user = User.create(email: 'boom@pop.com')

So now, our code looks like this:

spec/models/order_spec.rb

require 'spec_helper'

describe Order, 'associations' do
  it { expect(subject).to belong_to(:inventory_units) }
end

describe Order, '#cancel' do
  let(:order) { build(:order, status: 'complete', quantity: 5) }

  it 'changes the order status to cancelled' do
    order.cancel

    expect(order.status).to eq 'cancelled'
  end

  it 'restocks the inventory associated with the order' do
    order.inventory_unit.build
    order.inventory_unit.stub(:restock)

    order.cancel

    expect(order.inventory_unit).to have_received(:restock).with(5)
  end
end

Now, let’s drive the code:

class Order < ActiveRecord::Base
  belongs_to :inventory_unit, class_name: 'Inventory::Unit'

  def cancel
    inventory_unit.restock(quantity)
    update_attributes(state: 'cancelled')
  end
end

The reason we write it this way is because we’re only concerned about testing the model, we’re not interested in testing the inventory unit. We’ll let the inventory unit’s unit tests ensure that the methods are doing what they should. Notice we didn’t write code like this:

class Order < ActiveRecord::Base
  belongs_to :inventory_unit, class_name: 'Inventory::Unit'

  def cancel
    inventory_unit.update_attributes(
      on_hand: inventory_unit.on_hand + quantity
    )
    update_attributes(state: 'cancelled')
  end
end

With this code, the order model knows too much about the internals of the inventory unit model. By stubbing it, we ensure that the communication between the models has occurred, but neither model knows too much about the internals of the other.

Hopefully, this elucidates the purpose of stubs as well as providing a reasonable example. In practice, your model shouldn’t know about the internals of other models. If you’re testing a model with stubs, you can ensure communications occur between models and be certain that models aren’t aware of each others internals.

Mocks to come.