Minitest vs. RSpec in Rails

Rails defaults to minitest, but much of the community has adopted RSpec—which is right for you? In this article, William Kennedy compares RSpec and Minitest in a new Rails app.

Rails is a framework that comes with nearly everything included, focusing on conventions over configurations. Minitest is one of these conventions. Minitest is small and fast, and it provides many assertions to make tests readable and clean.

However, many alternatives to Minitest are available. The most popular one is RSpec.

RSpec has the same goals as Minitest but focuses on readable specifications describing how the application is supposed to behave with a close match to English. Minitest purports that if you know Ruby well, it should be enough.

The RSpec project focuses on behavior-driven development (BDD) and specification writing. Tests not only verify your application code but also provide detailed expressions that explain how the application should behave.

Minitest also supports test-driven development (TDD), BDD, mocking, and benchmarking. However, they have subtle differences, so we’ll go through an example app in both RSpec and Minitest to demonstrate the differences. Before we begin, let’s cover what all testing frameworks have in common.

Let’s Take a Step Back. What Is Testing?

Both TDD and BDD encourage a test-first approach, which means that you start by writing the test. You run the test, which should fail. Then you write the code that makes the test pass.

By writing a lot of tests to cover minor aspects of our overall program, we should eventually arrive at a nearly perfect system. Of course, this is software, so that doesn’t happen in reality. Yet, we can make drastic changes to our software with reduced risk by having many tests. Automated testing can be the difference between success and failure in a small startup pivoting to find market fit. In a large company, innovation does not have to slow down.

What are tests? We can answer this question by breaking testing down into three broad categories in the software world.

Unit Tests

A unit test is a small, automated test coded by a software developer to verify whether a small piece of production code – a unit – works as expected.

Many essays have been written about unit-tests, but one acronym you should be aware of is FIRST, which was first used by Tim Ottinger and Jeff Langr:

  • F - Fast - Tests must be fast to run. The faster they run, the more you’re likely to run them.
  • I - Isolated - Each test should have a single reason to fail
  • R - Repeatable - Tests should have the same result every time you run them.
  • S - Self-verifying - Results should not be open to human interpretation. They should be binary (e.g., Red/Green).
  • T - Timely - Write the tests before you write the code.

Integration Tests

The next broad category is integration tests. These tests verify that small pieces of code work.

Therefore, we can determine that A, B, C and D unit tests work independently, but how do you know that they work together?

Integration tests can sometimes be difficult to write and may be created as part of the bug-fixing process.

UI Testing

This branch of testing generally has the highest compute costs and runs a simulated user experience to test bugs, flows, and everything else. It’s most often used to replace or assist professional Quality Assurance testers.

Now that we’ve broken down software testing into broad categories, let’s go a step further and explore another important concept before comparing RSpec and Minitest.

Red, Green, and Refactor Lifecycle

In one of my first development jobs that practiced TDD, the senior developer used the three 'Gs'.

“Get it failing, get it working, and then get it Better.”

This made our workflow look like this:

Red, Green, and Refactor Lifecycle with Honeybadger Backdrop

Essentially, we write a test first then run it. The test will fail. Next, we write the code that makes the test pass. After the test passes, we can move on and improve the implementation. Slowly but surely, we arrive our ideal implementation.

This simple workflow seems counterintuitive because it is slower initially. However, in the long term, it is faster.

Comparing Minitest to RSpec

Chances are that if you are working on an existing codebase, the decision has already been made for you. If you are starting a project, you might be wondering if you should explore using RSpec or stick with Minitest. If you’re wondering if you should switch from one to the other, this is more nuanced and, depending on the size of your codebase, might not be worth it.

I hope you will see the actual code you will be writing, which will help your decision-making.

Here are the topics we’ll compare

Setup - Minitest Vs RSpec

To begin, we’ll create two similar apps and set up RSpec and Minitest.

Since Minitest comes installed with Rails, it’s simply using the Rails new command.

rails new rails_minitest

However, when setting up RSpec, we have to do more.

rails new rails_rspec

After the app is generated, we have to add the RSpec gems. This is where we encounter the first difficulty with RSpec.

Do we add the RSpec core gem or the rspec-rails gem?

It might seem obvious to select rspec-rails since we’re dealing with a Rails app, but this would be a mistake that I've seen more than once in my time as a consultant.

Setting up rspec-rails involves adding it to our Gemfile in two locations.

The project's readme covers all this.

# Gemfile
## For all the generators
group :development, :test do
 gem 'rspec-rails', '~> 5.0.0'
end

After adding this to your gem file, you can do the following:

bundle install

After installing, we can then run the rspec:install command.

rails generate rspec:install

This will create a spec folder, a .rspec file, and two additional files, spec/spec_helper.rb and spec/rails_helper.rb.

What is the difference between these two files?

The spec/rails_helper loads the Rails app, and spec/spec_helper is a lightweight configuration file for RSpec.

As a quick aside, you can speed up some of your RSpec tests if you avoid loading Rails for particular files. For example, if you are using plain-old Ruby objects(POROs) in your app that don’t need Rails, you can avoid adding Rails to that spec when running the test. This speeds up the feedback loop when writing the code and helps with the F(ast) in our FIRST principle, explained by Tim Ottinger and Jeff Langr.

Here is an example:

# app/services/hello_world.rb
class HelloWorld
 def say
  'Hello World'
 end
end
# spec/services/hello_world_spec.rb
# Usually, we require 'rails_helper' here, but there’s no need if we are not using Rails. 
require './app/services/hello_world'

RSpec.describe HelloWorld do

 describe '#hello' do
  it 'returns hello world' do
   expect(HelloWorld.new.say).to eq('Hello World')
  end
 end
end

Unit Testing: RSpec vs. Minitest

Now that we have set up our testing suite, we can compare how we write tests in each framework.

Let’s look at an example model test in RSpec. The convention is to write the test in plain English then in code.

require 'rails_helper'

RSpec.describe Article, type: :model do

 context 'validations' do
  article = Article.new
  article.valid?
  it 'must have a title' do
   expect(article.errors.messages[:title]).to include("can't be blank")
  end

  it 'must have a body' do
   expect(article.errors.messages[:body]).to include("can't be blank")
  end
 end
end

Run the test:

rspec --format documentation spec/models/article_spec.rb

When the test fails, we get the following output:

Article
 validations
  must have a title (FAILED - 1)
  must have a body (FAILED - 2)

Failures:

 1) Article validations must have a title
   Failure/Error: expect(article.errors.messages[:title]).to include("can't be blank")
    expected ["is too short (minimum is 5 characters)"] to include "can't be blank"
   # ./spec/models/article_spec.rb:9:in `block (3 levels) in <top (required)>'

 2) Article validations must have a body
   Failure/Error: expect(article.errors.messages[:body]).to include("can't be blank")
    expected [] to include "can't be blank"
   # ./spec/models/article_spec.rb:13:in `block (3 levels) in <top (required)>'

Finished in 0.02552 seconds (files took 1.3 seconds to load)
2 examples, 2 failures

Failed examples:

rspec ./spec/models/article_spec.rb:8 # Article validations must have a title
rspec ./spec/models/article_spec.rb:12 # Article validations must have a body

To get this output, you must run the RSpec command with the —format documentation option. The default is inline.

Next, let’s write the same test with Minitest.

require "test_helper"

class ArticleTest < ActiveSupport::TestCase
 setup do
  @article = articles(:one)
 end

 def test_validates_title
  @article.title = nil
  assert @article.valid?
  assert_equal ["can't be blank"], @article.errors[:title]
 end

 def test_validates_body
  @article.body = nil
  assert @article.valid?
  assert_equal ["can't be blank"], @article.errors[:body]
 end
end

Now run the test.

rails test test/models/article_test.rb
# Running:

F

Failure:
ArticleTest#test_validates_body [/Users/williamkennedy/projects/honeybadger/test_minitest/test/models/article_test.rb:17]:
Expected: ["can't be blank"]
 Actual: []

rails test test/models/article_test.rb:14

F

Failure:
ArticleTest#test_validates_title [/Users/williamkennedy/projects/honeybadger/test_minitest/test/models/article_test.rb:11]:
Expected: ["can't be blank"]
 Actual: []

rails test test/models/article_test.rb:8



Finished in 0.015373s, 130.0982 runs/s, 260.1964 assertions/s.
2 runs, 4 assertions, 2 failures, 0 errors, 0 skips

Straightaway, you will notice one thing. Minitest is just Ruby code, and RSpec is a new language to learn. Although the syntax is similar, RSpec tests can grow in length. There is also more mental overhead when it comes to RSpec, as your team has to define a convention for how tests should be structured.

For me, Minitest is just Ruby, which is a big win. Rails has built a convention into Minitest with the setup method.

The default Minitest file is already set up with the FIRST principle in mind.

UI Testing: RSpec vs. Minitest

Now let’s move on to another pillar of tests. Once again, the Rails default sets up UI tests nested under the test/system folder. However, RSpec has some setup involved, and there is no standard best practice.

A UI test is pretty involved. You write the instructions, such as click here and fill_in, that drive the interaction. They typically use a web driver, such as Selenium, to guide interactions.

They help test JavaScript behavior, recreate user journeys that might have caused errors, and even ensure that specific user flows are airtight.

They are, by their nature, more computationally expensive than regular tests, which is why we may opt for a headless CI tool.

Since Rails introduced system tests, RSpec has benefitted. Previously, we had to set this up manually.

Let’s take an example test. Create a file called spec/system/article_system_spec.rb and add the following code:

require 'rails_helper'

RSpec.describe 'Article', type: :system do
 it 'can be created' do
  visit '/articles/new'
  fill_in 'article[title]', with: 'Hello'
  fill_in 'article[body]', with: 'World'
  click_button 'Create'
  expect(page).to have_content 'Article was successfully created.'
 end
end

Running this test will produce what you expect. It will hook into the default web driver, Selenium, at the time of writing. RSpec allows you to change this per test or even all the specs by manually calling the driven_by method.

Here’s how:

require 'rails_helper'

RSpec.describe 'Article', type: :system do
 before do
  driven_by(:selenium_chrome_headless)
 end

 it 'should create article' do
  visit '/articles/new'
  fill_in 'article[title]', with: 'Hello'
  fill_in 'article[body]', with: 'World'
  click_button 'Create'
  expect(page).to have_content 'Article was successfully created.'
 end
end

The test will now run with Selenium_chrome_headless instead of Selenium which can speed up your tests.

Since the Capybara library drives the underlying tests, Minitest also has the same syntax.

require "application_system_test_case"

class ArticlesTest < ApplicationSystemTestCase
 setup do
  @article = articles(:one)
 end

 test "should create article" do
  visit articles_url
  click_on "New article"

  fill_in "Body", with: @article.body
  fill_in "Title", with: @article.title
  click_on "Create Article"

  assert_text "Article was successfully created"
  click_on "Back"
 end
end

However, there is a subtle difference in changing your web driver per test.

require "application_system_test_case"

class ArticlesTest < ApplicationSystemTestCase
 driven_by :selenium, using: :headless_chrome
 setup do
  @article = articles(:one)
 end

 test "should create article" do
  visit articles_url
  click_on "New article"

  fill_in "Body", with: @article.body
  fill_in "Title", with: @article.title
  click_on "Create Article"

  assert_text "Article was successfully created"
  click_on "Back"
 end
end

You can also change this globally:

# test/application_system_test_case.rb
require "test_helper"

class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
 driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
end

Conclusion

Let’s hope this helps you pick between RSpec and Minitest or learn the differences. I have tried to avoid bias as much as possible. There are strengths to both frameworks, but both have the same principles.

Write your tests first, make them pass, and in the long-term, you will have a much happier codebase.

The differences are subtle. With RSpec, you will write a lot more code, and there is a larger ecosystem of plugins to make things easier. However, when the ecosystem is larger and made up of different libraries, dependency trees can occasionally cause difficulty when it comes to upgrading your applications.

One aspect that cannot be ignored is performance.

Minitest is faster than RSpec, but the devil is in the details.

How much faster depends on how you measure. This is difficult to pin down because how you measure matters. Sampling bias(which is when a small sample size is used to come to a conclusion) may favor Minitest over RSpec by a factor of 10x. In other cases, the difference might be just 10%.

There is an interesting library that measures the raw performance of Minitest, RSpec, and Cucumber using Ruby Benchmark and finds the following for Ruby 3:

$ bundle exec ruby ./compare.rb
                 user     system      total        real
cucumber:   585.035884  22.566803  607.602687 ( 608.237973)
minitest:    18.208514   7.893526   26.102040 (  26.430622)
rspec:     2406.162561  12.497706 2418.660267 (2418.889164)
test_bench:  29.517226   8.272563   37.789789 (  38.133189)

Looking at this may cause you think Minitest is the winner by a landslide because it takes less time to run. However, in a real application, it might not be the case. The difference might only be 10%, or there may be no difference at all. Sampling bias is prevalent when it comes to comparing programmer tools due to a myriad of cultural, personal, and other reasons.

If your tests are slow, it may be due to number of other factors, such as database calls, memory, and maybe even network calls.

Choosing between RSpec and Minitest may just come down to personal preference.

Honeybadger has your back when it counts.

We combine error tracking, uptime monitoring, and cron & heartbeat monitoring into a simple, easy-to-use platform. Our mission: to tame production and make you a better, more productive developer.

Learn more
author photo

William Kennedy

I'm a professional Web Developer who likes to make awesome things. I enjoy contributing to open-source and helping small and medium sized businesses. My personal website is [here](https://williamkennedy.ninja).

“We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release.” 
Michael Smith
Try Honeybadger Free for 15 Days
Are you using Sentry, Rollbar, Bugsnag, or Airbrake for your monitoring? Honeybadger includes error tracking with a whole suite of amazing monitoring tools — all for probably less than you're paying now. Discover why so many companies are switching to Honeybadger here.
Try Honeybadger Free for 15 Days
Stop digging through chat logs to find the bug-fix someone mentioned last month. Honeybadger's built-in issue tracker keeps discussion central to each error, so that if it pops up again you'll be able to pick up right where you left off.
Try Honeybadger Free for 15 Days
"Wow — Customers are blown away that I email them so quickly after an error."
Chris Patton
Try Honeybadger Free for 15 Days