Benito Serna Tips and tools for Ruby on Rails developers

Three different ways of testing your Rails App

January 27, 2018

Do you have problems deciding what to test or how to test you rails app? Don’t worry, this is a very common problem… I think that there are a lot of reasons why this is hard for many people, but I have found that one of the problems is that are many ways to do it.

So here, I will try to compare three different ways to test the same feature, so you can decide which one is better for you.

What are we going to test?

Imagine that you are building a catalog that:

So, lets start with the first way…

Testing everything via “system tests”

Here is one way to write the tests for the specification…

feature "Users sees the catalog" do
  scenario "with all products sorted by name" do
    create :product, name: "product A"
    create :product, name: "product B"

    visit catalog_path

    expect_ordered_products("product A", "product B")
  end

  scenario "with the name, description and price of each product" do
    create :product, name: "product 1", description: "p1 - desc", price: 1000

    visit catalog_path

    expect(page).to have_content "product 1"
    expect(page).to have_content "p1-desc"
    expect(page).to have_content "$1,000.00"
  end

  scenario "with a product without price" do
    create :product, price: nil

    visit catalog_path

    expect(page).to have_content "Call for price"
  end

  scenario "with featured products" do
    create :product, name: "product A"
    create :product, name: "product B"
    create :product, name: "product Featured", feature: true

    visit catalog_path

    expect_ordered_products("product Feature", "product A", "product B")
  end
end

Look how in each scenario I try to describe the requirements of the specification and also that I try to avoid as much as I can the implementation details, for example I am using a create method that will create products for me, and also I am expecting to have a method expect_ordered_products instead of trying to jump in to the implementation details while I am still thinking in the behavior.

Pros:

Cons:

Testing controllers and views

This is a little harder because we are going to test the expected behavior in different places.

In the controller we can test…

RSpec.describe ProductsController do
  describe "GET index" do
    describe "assigns @products" do
      it "with the products sorted by name" do
        create :product, name: "B"
        create :product, name: "A"

        get :index
        expect(assigns(:products).map(&:name)).to eq(["A", "B"])
      end

      it "with the featured products first" do
        create :product, name: "B"
        create :product, name: "A"
        create :product, name: "Featured"

        get :index
        expect(assigns(:products).map(&:name)).to eq(["Featured", "A", "B"])
      end
    end
  end
end

In the view we can test…

RSpec.describe "products/index" do
  it "shows the name, description and price of the products" do
    assign(:products, [create :product, name: "Product-A", description: "A-desc", price: 1000])

    render

    expect(rendered).to match /Product-A/
    expect(rendered).to match /A-desc/
    expect(rendered).to match /$1,000.00/
  end

  it "shows 'call for price' for products without price" do
    assign(:products, [create :product, name: "A"])

    render

    expect(rendered).to match /Call for price/
  end
end

Here look also how in each test description I am trying to express the requirements of the specification. That is why in this case I think that we don’t need model tests, because we have are already tested what our specification says.

Pros:

Cons:

Testing a non Rails function

Imagine that you will call a function in the controller and the thing that it returns is all that you need to render the content… something like this:

def index
  @products = Catalog.get_products(Product)
end
# products/index.haml
- products.each do |product|
  %td= product.name
  %td= product.description
  %td
  - if product.has_price?
    = number_to_currency product.price
  - else
    Call for price

I think that if we test the Catalog.get_products method, we can test…

So translating this in to rspec, we have…

module Catalog
  RSpec.describe "Get products" do
    it "returns all the stored products sorted by name"
    it "returns the featured products first"

    describe "each product" do
      it "with a name, description and price"
      it "knows if has price or not"
    end
  end
end

Now lets continue with the implementation of our tests…

module Catalog
  RSpec.describe "Get products" do
    def get_products(store)
      Catalog.get_products(store)
    end

    it "returns all the stored products sorted by name" do
      products = get_products(store_with([
        product_with(name: "B"),
        product_with(name: "A")
      ]))

      expect(products.map(&:name)).to eq ["A", "B"]
    end

    it "returns the featured products first" do
      products = get_products(store_with([
        product_with(name: "B"),
        product_with(name: "A"),
        product_with(name: "Featured")
      ]))

      expect(products.map(&:name)).to eq ["Featured", "A", "B"]
    end

    describe "each product" do
      it "with a name, description and price" do
        product = get_products(store_with([
          product_with(name: "A", descrition: "A-desc", price: 1000)
        ])).first

        expect(product.name).to eq "A"
        expect(product.description).to eq "A-desc"
        expect(product.price).to eq 1000
      end

      it "knows if has price or not" do
        with_price, without_price = get_products(store_with([
          product_with(name: "A", price: 1000),
          product_with(name: "B", price: nil)
        ]))

        expect(with_price).to have_price
        expect(without_price).not_to have_price
      end
    end
  end
end

Look how we are not testing exactly for what our specification says, but we are testing for something that will help us implement the specification in a very easy way. I really prefer this approach over the other two, but you may like different things.

Some pros and cons…

Pros:

Cons:

Conclusion

Now is up to you… you have a little more knowledge to decide your way… There is no “true way” or at least I don’t know one. I just have one method that works best for me.

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.