Article for Junior developers on how to set up: Rails 5.2, RSpec 3.7, Factory Bot, Database Cleaner

Article was written 2018-08-09 and applies for current versions of gems

Configure fresh project

I’m assuming the reader has set up Ruby and Ruby on Rails on his computer after reading book Agile Web Development with Rails or https://guides.rubyonrails.org/getting_started.html

We are going to generate fresh Ruby on Rails project withot the native Ruby on Rails tests.

To display all options for fresh project you can lunch:

rails new -h

To generate fresh new project without Rails tests we will run:

rails new my_rspec_project_name --skip-test

Next open the Gemfile with your editor and insert into the section group :development, :test this gems:

# Gemfile

# ...

group :development, :test do

  # ...

  gem 'database_cleaner'
  gem 'factory_bot_rails'
  gem 'rspec-rails'
  gem 'faker'
end

# ...

It’s critical to have this gems in group :development, :test do not just group :test do as default commands like rails generate ... are running under development environment, therefore they will not pick up default config overrides by this gems unless you run: RAILS_ENV=test rails generate ...

Now install the gems:

bundle install

Now generate configuration files for RSpec

rails generate rspec:install

more at: https://github.com/rspec/rspec-rails

Now you should be able to generate Rails models with RSpec tests and FactoryBot factories:

rails generate model worker name:string age:integer

…after you lunch this generator you should see output like this:

invoke  active_record
create    db/migrate/20180809131148_create_workers.rb
create    app/models/worker.rb
invoke    rspec
create      spec/models/worker_spec.rb
invoke      factory_bot
create        spec/factories/workers.rb

As you can see this also generated db migration file db/migrate/20180809131148_create_workers.rb with content:

class CreateWorkers < ActiveRecord::Migration[5.2]
  def change
    create_table :workers do |t|
      t.string :name
      t.integer :age

      t.timestamps
    end
  end
end

So lets run the db migrations:

# migrations for develompent environment
rake db:migrate

# migrations for test environment
RAILS_ENV=test rake db:migrate

…this will generate the workers databaset table

Let write some test

open the spec/models/worker_spec.rb write your first “failing” test:

require 'rails_helper'

RSpec.describe Worker, type: :model do

  it do
    expect(1 + 200).to eq(0)
  end
end

Now run rspec spec. You should see output like this:

Failures:

  1) Worker should eq 0
     Failure/Error: expect(1 + 200).to eq(0)
     
       expected: 0
            got: 201
     
       (compared using ==)
     # ./spec/models/worker_spec.rb:6:in `block (2 levels) in <main>'

Finished in 0.00714 seconds (files took 0.59673 seconds to load)
3 examples, 1 failure, 1 pending

Great ! Now lets make this test pass. Change:

# ...
    expect(1 + 200).to eq(0)
# ...

to:

# ...
    expect(1 + 200).to eq(201)
# ...

Now you should see something like:

Finished in 0.00328 seconds (files took 0.61603 seconds to load)
3 examples, 0 failures, 1 pending

Don’t worry about the “pending” test. You can remove those later. What is important is that you have 0 failures. That means you have passing tests.

Working with Factories (FactoryBot)

https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md

Book Agile Web Development with Rails work with Fixture tests which valid way how to write tests but for purpouse of this and future tutorials we will use Factories.

You can read more about Factories in this article https://robots.thoughtbot.com/why-factories

We will use library FactoryBot

Before we start we need to configure one thing. Open spec/rails_helper.rb and add this line inside the RSpec configuration block:

# ...

RSpec.configure do |config|

  # ...

  config.include FactoryBot::Syntax::Methods
end

This will ensure we can work with Factory Bot syntax natively in our RSpec tests.

Open the spec/factories/workers.rb file and change autogenerated content from:

FactoryBot.define do
  factory :worker do
    name "MyString"
    age 1
  end
end

to

FactoryBot.define do
  factory :worker do
    name "Ezo"
    age 31
  end
end

Now open the spec/models/worker_spec.rb and add one more test:

require 'rails_helper'

RSpec.describe Worker, type: :model do

  # ....

  describe 'default worker details' do
    let(:worker) { create :worker }

    it 'should initialize worker with name and age' do
      expect(worker.name).to eq("Ezo")
      expect(worker.age).to eq(31)
    end
  end

  describe 'default worker details' do
    before do
      create :worker
    end

    it 'should initialize worker with name and age' do
      expect(Worker.count).to eq 1

      w = Worker.last

      expect(w.name).to eq("Ezo")
      expect(w.age).to eq(31)
    end
  end
end

If you run rspec spec all tests should pass 4 examples, 0 failures, 1 pending

Now these are just some stupid tests that will create a Worker in our database with some attributes and we just test if the attributes have those values.

In reality you will be testing more complex logic with RSpec such as

def trigger_money_transfer(account)
  account.balance = account.balance + 800
end

# ...
let(:bank_account) { create :account }

it "should transfer buch of money to my Account" do
  expect(bank_account.balance).to eq 0
  trigger_money_transfer(bank_account)
  expect(bank_account.balance).to eq 800
end
# ...

B.T.W. FactoryBot is a rewrite of older gem with the name FactoryGirl If you stumble upon any FactoryGirl mentions on the internet most of the functionality will work on FactoryBot

Database cleaner

DatabaseCleaner is a gem that will help you keep your database without records before every test run.

All you need to do is in spec/rails_helper.rb and add this lines inside the RSpec configuration block:

# ...

RSpec.configure do |config|

  # ...
  config.before(:suite) do
    DatabaseCleaner.strategy = :deletion
  end

  config.before(:each) do |example|
    DatabaseCleaner.clean
  end
end

In our context it will delete records from test DB before every test

There is more to this gem, there are different strategies so that the entire test suite is faster. Read more at https://github.com/DatabaseCleaner/database_cleaner

Faker

https://github.com/stympy/faker

Sometimes you want to have random data in your tests (E.g. random email address) as that helps you discover problems you would normally spot only in production when real data starts pouring into your system.

You can generate random data by using FactoryBot sequence syntax:

FactoryBot.define do
  factory :worker do
    sequence :name do |n|
      "Ezo#{n}"
    end

    age 31
  end
end

That will produce:

Ezo1
Ezo2
Ezo3
# ...

But still you will end up with pretty simmilar data in your system. There is a better way how to have truly random data with Faker gem:

Faker::Name.first_name
# => "Jonathan"
Faker::Name.first_name
#=> "Luigi"
Faker::Name.first_name
#=> "Crissy"
Faker::Number.number(2)
#=> "75"
Faker::Number.number(2)
#=> "48"

Faker offers lot of different “fake data” types. You can explore them here. Try to play around with variations to see what feels right.

What is common in Rails world is to combine Faker with Factory bot for generating random data:

in spec/factories/workers.rb

FactoryBot.define do
  factory :worker do
    name { Faker::Name.first_name }
    age 31
  end
end

So our test could look like (spec/models/worker_spec.rb)

require 'rails_helper'

RSpec.describe Worker, type: :model do

  # ....

  describe 'default worker details' do
    let(:worker) { create :worker }

    it 'should initialize worker with name and age' do
      expect(worker.name).to be_kind_of(String)
      expect(worker.name).to worker.name
    end
  end
end

Now again this is useless test not helping the developer much, I just want to show you that when you are dealing with random data you cannot just compare whether the result equals a string. You need to check if the result equals the state and type of an object.

You don’t want to have Random data all the time, I would even argue that most of the time it’s healthier to work with dereministic data. To understand why pls read this article

Discussion