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.
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.
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.
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 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 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.
Some of the Minitest strong points include:
assert_equal
, etc.) and spec-style syntax (describe
, it
, etc.).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).
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 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.
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
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.