Benito Serna Tips and tools for Ruby on Rails developers

How to test that you are creating a record without using the database

April 23, 2018

Do you want to know how you can use a test double or mock to test the creation of a record? Are you looking for an example of using mocks?… Maybe this post can help you =)

For better reference I have divided the task in 6 little steps:

  1. Using an object to represent the database
  2. How do you want to create the record?
  3. What is going to trigger the creation of the record?
  4. Testing that the store was called right
  5. Testing variations
  6. How you can actually use this function in your app

Note: All the examples are using ruby and the rspec library, but if you want to understand the concept, I think you will be able to translate the examples to an other language without much trouble

1. Using an object to represent the database

As we are not going to use a “real database” we need something to use as the database… So we are going to expect to have an object that will represent the database… I like to use the word “store” to represent it because I try to express that is my “storage mechanism” but you can use the name that you want…

So if you are writing a Catalog application I would start with something like…

class DummyProductsStore
end

2. How do you want to create the record?

Now that you have an object, you need think in the function or method do you want to be called on that object to perform the record creation…

For example, if you are writing a Catalog application and you want to save Products… you can expect to do something like

store = DummyProductsStore

attrs = {
  name: "My Product",
  description: "The best product!"
}

store.create(attrs)

#or
store.insert(attrs)

#or
store.save(attrs)

or maybe you expect a behavior like…

store = DummyProductsStore

product = Product.new(
  name: "My Product",
  description: "The best product!"
)

store.create(product)

#or
store.insert(product)

#or
store.save(product)

Personally I prefer to use the first way….

attrs = { name: "My Product", description: "The best product!" }
store.create(attrs)

… I think that is easier if you expect to use something like the ActiveRecord library to talk with the database.

So… now that we have decided how we want our “store” to behave, let’s write the method in our DummyProductsStore… but without nothing in it…

class DummyProductsStore
  def self.create(attrs)
  end
end

3. What is going to trigger the creation of the record?

Normally when you want to use a test double to test the creation of a record, is because you have a higher level function that will trigger the expected behavior…

For example in our catalog application we want “create products” or “add products” so.. maybe we are exposing something like…

Catalog.add_product(attrs, store)

#or

Catalog::AddProduct.call(attrs, store)

Again… I prefer the first one but you can have your own taste…

4. Testing that the store was called right

Now that you have defined who is going to trigger the creation of a record, now you know where do you need to test it.

So inside the test file for that method (or object…) you can have one test like…

class DummyProductsStore
  def self.create(attrs)
  end
end

def store
  DummyProductsStore
end

it "creates a record" do
  attrs = { name: "P1", description: "Super Product" }

  # This is the code that expects "store.create(attrs)"  to be called
  expect(store).to receive(:create).with(attrs)

  Catalog.add_product(attrs, store)
end

And you can also write another test to for the cases that you don’t want the store to be called…

describe "without name" do
  it "creates a record" do
    attrs = { name: "", description: "Super Product" }

    # This is the code that expects "store.create" not to be called
    expect(store).not_to receive(:create)

    Catalog.add_product(attrs, store)
  end
end

5. Testing variations

In some cases you will compute something with the attributes that you are receiving… in that case you can add more specific cases with something like…

describe "creates a record adding a slug" do
  example do
    attrs = { name: "P1", description: "Super Product" }
    expect(store).to receive(:create).with(attrs.merge(slug: "p1"))
    Catalog.add_product(attrs, store)
  end

  example do
    attrs = { name: "a FunKi Name", description: "Super Product" }
    expect(store).to receive(:create).with(attrs.merge(slug: "a-funky-name"))
    Catalog.add_product(attrs, store)
  end
end

6. How you can actually use this function in your app

You have designed the expected behavior for the storage system… This means that you can use your function with a “real” storage system that behaves in the same way…

Note: Maybe for the “real” storage you will need or want to write some integration tests with the actual storage device.

For example, if you are using active record you can just use the active record class directly… because it behaves in the same way =)

class Product < ActiveRecord::Base
end

store = Product

Catalog.add_product(attrs, store)

… And I think that’s all for now… I hope it helps =)

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.