loading...

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

bhserna profile image Benito Serna Updated on ・4 min read

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 examples 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 =)


This post was originally posted on http://bhserna.com

Discussion

markdown guide
 

Great! I've thought this several years ago but no solution.

I tried simple case, it really works.

But if I want to test a blog's title validation of presence, how could I do it. The DummyStore cannot validate, any suggestion?

Thanks your share.

  it "should not creates a record" do
    attrs = { title: "", body: "Super Product" }

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

    BlogList.add_blog(attrs, store)
  end  
 

Start by trying to express in your tests what the client will need...

For example if you need validation... Maybe at the client you will need something to know if the action was successful or not, and if it was not successful you will need to show why it was not successful...

What I do in this cases is something like this...

describe "without title..." do
  it "does not returns success" do
    attrs = { title: nil, description: "Super post..." }
    status = add_blog(attrs, store)
    expect(status).not_to be_success
  end

  it "does not creates the record" do
    attrs = { title: nil, description: "Super post..." }
    expect(store).not_to receive(:create)
    add_blog(attrs, store)
  end

  it "returns an error" do
    attrs = { title: nil, description: "Super post..." }
    status = add_blog(attrs, store)
    expect(status.form.errors[:title]).to eq ["can't be blank"]
  end
end

About where you should do the validation, is up to you and your other
use cases... Why don't you try to implement something to make this pass?

As a hint... You can use ActiveModel, but try not to delegate this
responsibility to the store.

I have this post with a little template that I use, that maybe can help you...
bhserna.com/2018/a-little-template...

And this is a gist with a similar example that I did sometime ago...
gist.github.com/bhserna/925cba5b8b...

=)

 
class Blog 
  validates :title, presence: true
end

# no validation 
Blog.save(attrs)

# has validation
blog = Blog.new
blog.save(attrs)

Instance method save trigger validation, but class method save not trigger validation. Is this true or I do something wrong? I think it should trigger validation too.

I've got the answer. The validation works but seems not easy to test.

!@blog.id.nil? #this works, any better code?

I can plan two stages test. First stage don't test validation. In second integration test I can use model test to do validation test. Do you agree with me? :)

I would test the validation part, just as I said in the comment... This
is because, most of the time I don't put validation code in the Rails
models.

I would put the behavior in a kind of form object, in fact, you could use
ActiveModel if you want...

Two possible solutions could be...

Without ActiveModel...

module Blog
  def self.add_post(attrs, store)
    form = Form.new(attrs)

    if form.title.nil?
      form.add_error!(:title, "can't be blank")
      ErrorStatus.new(form)
    else
      store.create(form.to_h)
      SuccessStatus
    end
  end

  class Form
    attr_reader :title, :body, :errors

    def initialize(params = {})
      @title = params["title"]
      @body = params["body"]
      @errors = {}
    end

    def to_h
      { title: title, body: body }
    end

    def add_error!(field, error)
      errors[field] ||= []
      errors[field] << error
    end
  end

  class ErrorStatus
    attr_reader :form

    def initialize(form)
      @form = form
    end

    def success?
      false
    end
  end

  class SuccessStatus
    def self.success?
      true
    end
  end
end

With ActiveModel...

module Blog
  def self.add_post(attrs, store)
    form = Form.new(attrs)

    if form.valid?
      store.create(form.to_h)
      SuccessStatus
    else
      ErrorStatus.new(form)
    end
  end

  class Form
    include ActiveModel::Model
    attr_accessor :title, :body
    validates_presence_of :title

    def to_h
      { title: title, body: body }
    end
  end

  # ...
end

Looks nice, thanks.

Then you don't write rails AR validation, is this true?

Is really rare... but I do use ActiveModel for form objects very often