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:

  • Shows all products sorted by name.
  • Show for each product the name, description and price.
  • If a product has no price it shows a label with “Call for price”.
  • If there are featured products, it shows them first.

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:

  • I think is the must “real” way of testing the behavior.
  • Tests will be working if we refactor the internals of our application.

Cons:

  • If we do this for every feature, our test suite are gonna get pretty slow.
  • At this level we have not enough feedback about the design of our code.
  • Tests are hard to set up.
  • Tests can fail if we change html code.

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…

  • That all stored products are assigned to the @products instance variable.
  • That those products are sorted by name.
  • That the featured products are at the top of the list.
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…

  • That it shows the name, description and price of the products.
  • That if a product has no price it should show “Call for price”.
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:

  • Test are faster.
  • You will have more feedback about the design of you code

Cons:

  • Tests are by way harder to imagine.
  • Tests are still hard to setup
  • If we refactor the internals of our app, we can break our tests.
  • If we change the html we can break our tests.

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…

  • That returns all the stored products.
  • That returns the feature products first.
  • That each product has a name, description and price.
  • That each products know if a product has price.

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:

  • Test are simpler because we are testing just a function.
  • As you are passing the Product class, in your test you can replace the concrete implementation with an object that implements just the behavior that you would use. This will make your tests fast.
  • You are decoupling you application logic from Rails.
  • In my humble opinion now you don’t need to write tests for your controller, model and views.

Cons:

  • You are creating a new abstraction.
  • Maybe at the beginning it will not be very clear how to test that behavior.

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.