Minitest vs RSpec for testing Rails applications

Table of contents

Testing is an integral part of the software development lifecycle. And a testing framework is one of a key decision for the project and the team. One that's usually hard or expensive to reverse.

In Ruby the choice comes down to Minitest and RSpec. Each has its own strengths and use cases, making the choice between them not always straightforward. One usually pairs with fixtures and one with factory approach to create test data.

I used RSpec almost exclusively at work and for my freelance clients. And I used Minitest with fixtures on all my little Rails apps. So let's see the key differences, benefits, and examples of both frameworks.

About Minitest

Minitest is a lightweight, simple-to-use testing framework for Ruby. It provides a fast and straightforward way to write unit and integration tests. Minitest emphasizes simplicity and clarity in test code, making it easy to read and maintain.

Initially introduced as part of Ruby's standard library since Ruby 1.9 in 2007, Minitest was designed as a simplified successor and replacement for Ruby's older Test::Unit framework. Ryan Davis developed Minitest to provide a minimalistic and efficient solution that aligns with Ruby's philosophy of simplicity and elegance.

Today, Minitest is widely adopted in Ruby gems and the test framework of choice of both DHH, author of Rails, and Steven R. Baker, the original author of RSpec. It's also now the default testing framework in Rails.

About RSpec

RSpec is a widely-used testing framework for Ruby and Ruby on Rails applications. It provides a domain-specific language (DSL) that makes writing tests more of behaviour specification as opposed to just asserting facts about the system.

The beauty in RSpec is in its DSL that let's you structure your test suite as a specification with as many nesting as you might need. It's also a very practical choice due to its popularity for testing Rails applications. They are many books, tutorials, and resources to learn RSpec way of testing.

One interesting thing about RSpec is that its original author Steven R. Baker says that he created RSpec for learning purposes rather than the practical utility as a test framework. Nevertheless, it's the number one test fromework for Rails in terms of wide adoption.

Creating test data

Rails offers two ways to create data. Active Record and fixtures. Fixtures are pre-loaded data sets while regular Active Record let's you create additional objects when needed. Fixtures fit perfectly well with the Minitest way of doing things.

RSpec practitioners on the other hand embraced the factory-pattern to create test data with a library called FactoryBot. You can think of this as using Active Record on steroids, making you create your test objects without too much work.

Fixtures

Fixtures in Rails are predefined sample data one can use later in the test suite. You might have noticed that newly generated Rails applications feature YAML files in the test/fixtures directory representing our models. They are the most common fixtures, but not the only ones.

The way I think about fixtures is that they are a small representation of your application world. They are a small snapshot of a running application at a specific time. We design them to cover 60-80% cases while keeping the amount of fixtures small.

Here's an example of defining a fixture that will be loaded in our tests by default:

# test/fixtures/users.yml
<% password = "test123456" %>

joe:
  name: Joe
  email: <%= Faker::Internet.unique.email %>
  encrypted_password: <%= Devise::Encryptor.digest(User, password) %>
  confirmed_at: <%= DateTime.now %>
  time_zone: "Berlin"

And here is how we would use a fixture in a test:

class UserTest < ActiveSupport::TestCase
  setup do
    @user = users(:joe)
    @user.update(my_attribute: "New value")

    @other_user = User.create!(my_attribute: "Passed value")
  end

  test "#my_attribute" do
    assert_equal "New value", @user.my_attribute
    assert_equal "Passed value", @other_user.my_attribute
  end
end

Factories

Factories too come with object definition and even variants:

factory :user do
  first_name { "Joe" }
  last_name { "Silver" }
end

However, a factory is just a mere default, a shortcut, to produce test data at test time:

it "downcases the location on save" do
  user = create(:user, first_name: "Peter")

  expect(user.first_name).to eq "Peter"
end

We usually try to create objects with attributes that are being tested, so this information is close to its expectation.

Pros and cons

Minitest

Some of the Minitest strong points include:

My personal favourite is likely how easy is to maintain a test suite long-term.

Some of the disadvantages include lack of adoption among Rails agencies and startups (for career purposes), limited nesting options (like multiple describe blocks), and required assertion nesting (wrapping assertions).

RSpec

Some of the RSpec strong points include:

I like the easy nesting which can arguably be overused but makes things a little bit more organized at times. Some things that are cumbersome in Minitest write nicely in RSpec.

The main disadvantage is then slower execution speed, extra dependencies, and the effort keeping factories fast.

Rails integration

Minitest with fixtures

Rails comes with Minitest and fixtures by default. You don't need to configure anything to start testing. In a Rails app, the test directory typically looks like this:

test/
├── controllers/
├── fixtures/
├── helpers/
├── integration/
├── mailers/
├── models/
├── system/
└── test_helper.rb

Rails also provides built-in rake tasks to run tests:

rails test
rails test test/models/user_test.rb
rails test test/controllers/posts_controller_test.rb
rails test:system

Fixtures are loaded automatically, but you are also free to load just specific ones for the test at hand:

# test/models/user_test.rb
require "test_helper"

class UserTest < ActiveSupport::TestCase
  fixtures :users

  test "user fixture should be valid" do
    alice = users(:alice)
    assert alice.valid?
    assert_equal "Alice Smith", alice.name
  end
end

Rails will also generate your test files as part of its scaffold generator.

There is a bit more to say about the exact integration. Get a copy of Test Driving Rails to learn more.

RSpec and factories

Rails currently cannot generate new applications using RSpec as it's not part of Rails. However we can at least skip the generation of Minitest scaffolded files:

$ rails new myapp --skip-test

Then we can add rspec-rails to Gemfile alongside FactoryBot, Faker, and other test libraries we want to use:

# Gemfile
group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
end

And continue with RSpec installation:

$ rails generate rspec:install

This command generates the necessary directory structure and configuration files including helper files.

To continue using RSpec with scaffolding, we can set config.generators to rspec :

config.generators do |g|
  g.orm             :data_mapper, migration: true
  g.template_engine :haml
  g.test_framework  :rspec
end

RSpec tests are called specs and are typically placed under the spec/ directory, structured similarly to the Rails application:

spec/
  models/
  controllers/
  factories/
  features/
  helpers/
  requests/
  views/

To run the test suite we would call rspec directly instead of bin/rails test :

$ bundle exec rspec
$ bundle exec rspec spec/models/user_spec.rb

Summary

RSpec is a powerful, expressive testing framework for Ruby and Rails. Together with FactoryBot and Faker is the most common way of testing Rails applications today. On the other hand Minitest with fixtures is a simple capable, easy to learn default and part of Rails Omakase menu. I encourage you to give it a go on your next project.

Author
Josef Strzibny
Hello, I am Josef and I am on Rails since its 2.0 version. I always liked strong conventions and the Rails Omakase docrine. I am author of Kamal Handbook and Test Driving Rails. I think testing should be fun, not a chore.

© Test Driving Rails Blog