Benito Serna Tips and tools for Ruby on Rails developers

Writing unit tests as feature tests

February 21, 2018

Are you struggling trying to define what to test on your unit tests? Maybe is easier for you to identify what to test on your feature tests or system tests because the user story or approval criteria, in a certain way “tells you” what to test or what is expected to happen.

But with your unit tests is different…

…because now you want to test that class or module that you need to accomplish the total feature. And maybe you don’t know at what level or detail you have to write those tests… Do you need to test the method or the class inside that module? How do you separate the scenarios?…is not that easy!!

But what if instead of trying to write test for your models, presenters, controllers, etc… you write tests for the features that you need to implement, but not as a system tests, but as unit tests!

Maybe now you will have to define and design your “units”… but is not that hard, and I think that this is the point of “unit” testing (don’t you think?)…

Let’s try with an example…

Imagine you are working on a CRM and there you need to write a feature to register a company in the system. And to register that company you need the name and the logo of the company.

So to test that with a “feature” test or “system” test, you can create a spec/features/create_company_spec.rb file and there you can write something like…

feature "Create company" do
  scenario "with name and logo" do
    visit new_company_path

    fill_in :name, with: "C1"
    attach_file :logo, "/fixtures/logo.png"

    expect(page).to have_content "Successfully created"
    expect(page).to have_content "C1"
    expect(page).to have_current_path "/companies/c1"
  end

  scenario "without name" do
    visit new_company_path
    fill_in :name, with: ""
    expect(page).to have_content "Name can't be blank"
  end
end

Let’s check what we are testing here…

We are testing that…

So, can we design some “plain ruby” functions that can almost implement this feature?

Ooo yes we can =)… You can create an spec file spec/crm/create_company_spec.rb

And write something like…

module CRM
  RSpec.describe "Create company" do
    def new_company_form
      CRM.new_company_form
    end

    def create_company(params, store)
      CRM.create_company(params, store)
    end

    attr_reader :store, :params, :logo

    before do
      @store = DummyStore
      @params = { "name" => "D1", "logo" => @logo = Object.new }
    end

    it "has a form" do
      form = new_company_form
      expect(form.name).to eq nil
      expect(form.logo).to eq nil
    end

    describe "creates a record adding the slug" do
      example do
        expect(store).to receive(:create).with(name: "D1", logo: logo, slug: "d1")
        create_company(params, store)
      end

      example do
        name = "uno Dos trEs"
        slug = "uno-dos-tres"
        expect(store).to receive(:create).with(name: name, logo: logo, slug: slug)
        create_company(params.merge("name" => name), store)
      end
    end

    it "returns success" do
      status = create_company(params, store)
      expect(status).to be_success
    end

    describe "without name" do
      it "does not returns success" do
        status = create_company({}, store)
        expect(status).not_to be_success
      end

      it "does not creates the record" do
        expect(store).not_to receive(:create)
        create_company({}, store)
      end

      it "returns a blank error" do
        status = create_company({}, store)
        expect(status.form.errors[:name]).to eq ["no puede estar en blanco"]
      end
    end
  end
end

And as you can see, here we are testing that…

Is not the same, but is almost the same =)… and if you can build something like this, then you can use them in your controller like this…

class CompaniesController < ApplicationController
  STORE = Company

  def new
    form = CRM.new_company_form
    render locals: { form: form }
  end

  def create
    status = CRM.create_company(params[:company], STORE)

    if status.success?
      redirect_to admin_companies_path, notice: "La empresa ha sido creado exitosamente."
    else
      render :new, locals: { form: status.form }
    end
  end
end

…with confidence that now you don’t need to test every path of your application on your controller or system test. Just write some integration tests when you feel the need to write them =).

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.