Benito Serna
Ruby/Rails, TDD, Software...

Writing unit tests as feature tests

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"

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

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

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

    attr_reader :store, :params, :logo

    before do
      @store = DummyStore
      @params = { "name" => "D1", "logo" => @logo = }

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

    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)

      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)

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

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

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

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

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 }

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

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

…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 =).