Benito Serna Tips and tools for Ruby on Rails developers

Slow Tests?... How to remove the database from your tests and make them fast!

March 10, 2018

Is a common problem in a Rails app to have very slow test suites. There are a lot of test suites that need more than 10 or even 30 minutes to run!…

Is your app in this group?

If it is and you want to do something about it, I have a some tips for you!

One of the things that makes your tests run slow is the usage of the database on them, maybe you have heard about it… And for that reason some people recommend that if you don’t need to create a record, is better to just use the instance of the object without creating the record, for example, to calculate the total of an order…

# some people recomend this...
describe "Order" do
  it "has a total" do
    order = Order.new(
      items: [
        OrderItem.new(itemprice: 100, quantity: 3),
        OrderItem.new(price: 200, quantity: 1)
      ]
    )

    expect(order.total).to eq 500
  end

  # instead of this...
  it "has a total" do
    order = Order.create
    order.items.create(price: 100, quantity: 3)
    order.items.create(price: 200, quantity: 1)
    expect(order.total).to eq 500
  end
end

And thats ok, I think…

But following the same example, lets focus now on the action when the user wants to add the same item again to the order. Where would you put that logic?… Let’s say that you want to put that behavior in your model, so let’s write a test for this…

describe "add item" do
  example do
    order = Order.new
    order.add_item(Item.new(id: 1234, name: "I1", price: 110))

    order_item = order.items.first
    expect(order_item.item_id).to eq 1234
    expect(order_item.name).to eq "I1"
    expect(order_item.price).to eq 110
    expect(order_item.quantity).to eq 1
    expect(order_item.total).to eq 110
  end

  example "add the same item twice" do
    order = Order.new
    item = Item.new(id: 1234, name: "I1", price: 110)
    order.add_item(item)
    order.add_item(item)

    order_item = order.items.first
    expect(order.items.count).to eq 1
    expect(order_item.quantity).to eq 2
    expect(order_item.total).to eq 220
  end
end

Hey, we did it =) … Hmmm…but where do we want to save the OrderItem? in the controller?… we can do it with something like…

class OrderItemsController < ApplicationController
  def create
    item = Item.find(params[:id])
    order = Order.find(params[:order_id])
    order.add_item(item)
    order.save
    redirect_to order_path(order)
  end
end

Do you feel the need to test that the order is going to be saved in the right way?… Well I actually feel that need, we have modified the state of at least two objects and for me is not very clear if the data will be saved correctly… I would prefer to call save inside order.add_item and be able to test that…

But how do we test that, without using the database in our tests?

Before saying you how… I will start by saying you what kind of operations from ActiveRecord I can trust I am using fine…

In the last example I wasn’t sure how active record would save the data… but if instead we were doing something like…

OrderItem.create(
  order_id: order.id,
  item_id: item.id,
  quantity: 1,
  price: item.price
)

# or

OrderItem.update(order_item.id, quantity: 2)

Well.. I think that in these two examples is easier to trust that we are using ActiveRecord right, don’t you think?… So, If we are sure that ActiveRecord can handle this operation… What if instead of been tied to it, we take the OrderItem class as a dependency? and expect the usage of this two methods…

So let’s try to change our test to do it…

describe "add item" do
  it "creates an order item record" do
    order = Order.new
    item = Item.new(id: 1234, name: "I1", price: 110)

    expect(OrderItem).to receive(:create).with(
      order_id: order.id,
      item_id: item.id,
      quantity: 1,
      price: 110
    )

    order.add_item(item, OrderItem)
  end

  it "updates the record quantity when the item is added twice" do
    item = Item.new(id: 1234, name: "I1", price: 110)
    order_item = OrderItem.new(id: 12345, item_id: item.id, quantity: 1)
    order = Order.new(items: [order_item])

    expect(OrderItem).
      to receive(:update).
      with(order_item.id, quantity: 2)

    order.add_item(item, OrderItem)
  end
end

So now we are testing that we are creating or updating our records without actually creating or updating them, because now we are injecting a dependency that we trust that will do the job for us if we send the right message =)

If we do this change, now we can use our Order#add_item method on out controller without the need of calling save there…

class OrderItemsController < ApplicationController
  def create
    item = Item.find(params[:id])
    order = Order.find(params[:order_id])
    order.add_item(item, OrderItem)
    redirect_to order_path(order)
  end
end

Is this design better?… Hmmm I think it depends on who is deciding that…

Is this design simpler?… Hmmm I think it is, because now we are depending on a simpler behavior. But well that’s why I think =)

Will this design improve the performance of you tests?… Yes, for sure =)

Other thing that slows our tests, is to require the whole rails, to test our business logic… But the good news are that if we follow this approach, in a lot of cases you will not need it =)

For example… What I don’t like much about this last approach is that now I am not sure if the method add_order should belongs to the order… Maybe we can have a function named add_item_to_order that will receive the order, the item and the OrderItem class…

Maybe something like this…

describe "add item to order" do
  it "creates an order item record" do
    order = Order.new
    item = Item.new(id: 1234, name: "I1", price: 110)

    expect(OrderItem).to receive(:create).with(
      order_id: order.id,
      item_id: item.id,
      quantity: 1,
      price: 110
    )

    add_item_to_order(order, item, OrderItem)
  end

  it "updates the record quantity when the item is added twice" do
    item = Item.new(id: 1234, name: "I1", price: 110)
    order_item = OrderItem.new(id: 12345, item_id: item.id, quantity: 1)
    order = Order.new(items: [order_item])

    expect(OrderItem).
      to receive(:update).
      with(order_item.id, quantity: 2)

    add_item_to_order(order, item, OrderItem)
  end
end

If we make this little change, and if you see the test in detail… Now we don’t need any special feature from rails, all we need is three “values”, the Item, the Order and the OrderItem, and an object that will manage the storage of the OrderItems… So now we can replace the active record objects with this…

class FakeItem
  attr_reader :id, :name, :price
  def initialize(attrs)
    @id = attrs[:id]
    @name = attrs[:name]
    @price = attrs[:price]
  end
end

class FakeOrder
  attr_reader :items
  def initialize(attrs)
    @items = attrs[:items]
  end
end

class FakeOrderItem
  attr_reader :item_id, :order_id, :quantity, :price
  def initialize(attrs)
    @item_id = attrs[:item_id]
    @order_id = attrs[:order_id]
    @quantity = attrs[:quantity]
    @price = attrs[:price]
  end
end

class FakeOrderItemsStore
  def self.create(attrs)
    # no needed behavior for now
  end

  def self.update(id, attrs)
    # no needed behavior for now
  end
end

So we can change our test to use those “fake” objects…

describe "add item to order" do
  it "creates an order item record" do
    item = FakeItem.new(id: 1234, name: "I1", price: 110)
    order = FakeOrder.new

    expect(FakeOrderItemsStore).to receive(:create).with(
      order_id: order.id,
      item_id: item.id,
      quantity: 1,
      price: 110
    )

    add_item_to_order(order, item, FakeOrderItemsStore)
  end

  it "updates the record quantity when the item is added twice" do
    item = FakeItem.new(id: 1234, name: "I1", price: 110)
    order = FakeOrder.new(items: [
      FakeOrderItem.new(id: 12345, item_id: item.id, quantity: 1)
    ])

    expect(FakeOrderItemsStore).
      to receive(:update).
      with(order_item.id, quantity: 2)

    add_item_to_order(order, item, FakeOrderItemsStore)
  end
end

Jeje but now our test looks very ugly =( … let’s make some factory methods to make it look better…

describe "add item to order" do
  def order_with(attrs)
    FakeOrder.new(attrs)
  end

  def item_with(attrs)
    FakeItem.new(attrs)
  end

  def order_item_with(attrs)
    FakeOrderItem.new(attrs)
  end

  def order_items_store
    FakeOrderItemsStore
  end

  it "creates an order item record" do
    item = item_with(id: 1234, name: "I1", price: 110)
    order = order_with(items: [])

    expect(order_items_store).to receive(:create).with(
      order_id: order.id,
      item_id: item.id,
      quantity: 1,
      price: 110
    )

    add_item_to_order(order, item, order_items_store)
  end

  it "updates the record quantity when the item is added twice" do
    item = item_with(id: 1234, name: "I1", price: 110)
    order = order_with(items: [
      order_item_with(id: 12345, item_id: item.id, quantity: 1)
    ])

    expect(order_items_store).
      to receive(:update).
      with(order_item.id, quantity: 2)

    add_item_to_order(order, item, order_items_store)
  end
end

And where should we put this function?… well you can just create a module, maybe something like Orders, and put it there… Actually you don’t have to worry to much about the module name, because as this function will receive all the things it needs as parameters, if you later decide to put it elsewhere you will be able to just change the code to that other place =)…

So now, your controller would look something like this…

class OrderItemsController < ApplicationController
  def create
    item = Item.find(params[:id])
    order = Order.find(params[:order_id])
    Orders.add_item(order, item, OrderItem)
    redirect_to order_path(order)
  end
end

Not so bad, don’t you think?

And that’s it!!… this is a not so hard way of putting the database or really any storage mechanism out of your business logic. And make your tests run fast!!

And here… some possible questions that you may have at this moment…

How fast will my tests be if I do this?

Well it depends on the code you are testing, but I can tell you that in the last two apps that I have applied this method one runs something like 1,000 tests in 2 seconds and the other runs 1,600 in more less 6 seconds.

Are we introducing too much “fake code”?

Here the answer also depends on how you see it. Because this “fake code” is much, but really much much less code, than the ActiveRecord library’s code and the database’s code…

What I think is that this separation makes you more conscious and also makes your code more explicit about your dependencies… What are you really using from ActiveRecord? If you need a better/faster library or a better/faster database, how easy would be to replace it?

How can I handle breaking changes in ActiveRecord?

As we are not testing ActiveRecord (or the thing your are using to access your data…) you will need to be careful to detect if there are changes that break the behavior that you are expecting.

The good news are that…

And if you are not confident enough you can write some tests for your store to see if it has the behavior that you want, and with the rspec “shared examples” you can run them against your fake store and your real store.

For example…

RSpec.shared_examples "OrderItemsStore" do |parameter|
  it "creates a record" do
    store.create(attrs)
    #...
  end

  # ...
end

RSpec.describe FakeOrderItemsStore do
  attr_reader :store

  before do
    @store = FakeOrderItemsStore.new
  end

  it_behaves_like "Order Items Store"
end

RSpec.describe OrderItem do
  def store
    OrderItem
  end

  it_behaves_like "Order Items Store"
end

RSpec.describe YourNewOrderItemStore do
  def store
    YourNewOrderItemStore
  end

  it_behaves_like "Order Items Store"
end

This tests will need to interact with the actual storage mechanism but you will have much less tests here, because you are not going to re-implement ActiveRecord, you will only use a small part of it.

Are we testing anything with all that “fake code”?

Well, here we are testing that we are sending the right message to the “store”… Is similar to what Sandy Metz calls “testing outgoing messages with side effects” in her talk The Magic Tricks of Testing.

But one cool thing, is that in real apps, this addorderto_item functionality will need much behavior soon… maybe send an email, track some analytics data, send a notification… who knows?… But now you have a place were you can put and/or test that code.

Are we introducing too much “test induced damage”?

Hmmm, that really is a question to you… what is good code?, what is bad code?, do you prefer to have slow tests?, do you prefer not to write tests?, do prefer to look for faster computers, or parallelize your code?

Is this just a procedure? I preffer Object Oriented Programming!!

Well, actually is kind of a procedure, but also is kind of a function, and we are passing objects… and nothing is preventing you from creating new objects inside that function/procedure when you think is needed.

Also about procedures… Avdi Grimm has a good article about them: Enough With the Service Objects Already.

Yeah! but how can I apply this in my current app?

You can start in a similar way… In the next change you will make to your app, pick a little functionality and put it behind a module and a function =)

And that’s all for now. I hope you can take something from this message =)

Related articles

Weekly tips and tools for Ruby on Rails developers

I send an email each week, trying to share knowledge and fixes to common problems and struggles for ruby on rails developers, like How to fetch the latest-N-of-each record or How to test that an specific mail was sent or a Capybara cheatsheet. You can see more examples on Most recent posts or All post by topic.