home articles hire me

Lost in Minitest? Start here!

I have a confession to make: I have never used Minitest in the seven years I’ve been a professional programmer.

I’ve always used the other framework.

But earlier this year, I started working with a client whose application relied solely on QA instead of automated tests. In an effort to bring the team peace of mind during releases, I started adding tests to the most critical parts of the application.

Lured by the promise of speed and wide adoption, I suggested we try Minitest.

As I started working on writing my first tests, I hit an unexpected roadblock.

Minitest (lack of) onboarding

After writing several hundred tests, I can confidently say that Minitest’s biggest weakness is its onboarding. The information is so awfully scattered, so sparse and so obscure that it makes navigating your tax returns friendlier in comparison.

As a newcomer, I would have loved (let’s forget about love, I would have needed!) to get the main information right off the bat:

Instead, Minitest’s official repository is a piece of writing more akin to Joyce’s Ulysses than to a technical documentation.

Programmers get welcomed with a list of footer-worthy links, some congratulatory quotes, and hints that you should get a mental health check for having ever used RSpec.

You have to scroll past this cruft to get the first useful nugget of information: a breakdown of Minitest main components.

Minitest 101

Minitest is a prominent testing framework for Ruby code. It’s the default testing framework embedded by default in Ruby and Ruby on Rails.

If you generate a new Rails app today, or if you have an existing app with no other test framework configured, you can run rails test, and Rails will find the test folder and check for test files.

Otherwise, it’s just a matter of adding gem 'minitest' to your Gemfile, and off you go. Minitest provides several components, each addressing a specific need. Here are a few:

Minitest uses the concept of assertions which lets you verify that the expected result matches the actual result:

Check that this condition holds true.

Coming from RSpec, I’m used to the concept of expectations which take the opposite perspective of Minitest’s assertions:

This object should behave this way.

Minitest syntax flavors : everything, everywhere, all at once

One thing that slowed down my adoption is that Minitest offers multiple styles of syntaxes. You can write the same test in several ways, sometimes even mixing styles together. Each style lives in its own module or extension.

RSpec is either loved or hated for its DSL, but it gives me one major advantage: I don’t have to decide for every test which style to use. I use the standard and I move on.

With Minitest, though, I can choose from:

Let’s write some tests so you get my point.

Writing my first tests with Minitest

Let’s say I have this plain Ruby User class:

  class User
    attr_accessor :first_name, :last_name

    def initialize(first_name:, last_name:)
      @first_name = first_name
      @last_name = last_name
    end

    def full_name
      "#{first_name.capitalize} #{last_name.capitalize}"
    end
  end

And I want to write a simple test for it:

  require "minitest/autorun"
  require_relative "user"

  class UserTest < Minitest::Test
    def setup
      @user = User.new(first_name: "buffy", last_name: "summers")
    end

    def test_returns_the_full_name
      assert_equal "buffy summers", @user.full_name
    end
  end

In this case, I’m using plain Minitest. It’s the default syntax, with the simplest setup.

Several remarks:

Here’s the output for this first test:

  lab/minitest-post → ruby user_test.rb
  Run options: --seed 64661

  # Running:

  .

  Finished in 0.001239s, 807.1025 runs/s, 807.1025 assertions/s.
  1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

If my test were to failed:

  lab/minitest-post → ruby user_test.rb
  Run options: --seed 22116

  # Running:

  F

  Failure:
  UserTest#test_returns_the_full_name [user_test.rb:10]:
  Expected: "buffy summers"
    Actual: "Buffy Summers"

  bin/rails test user_test.rb:9

  Finished in 0.001472s, 679.3478 runs/s, 679.3478 assertions/s.
  1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

The same test, but in Rails

  class User < ApplicationRecord
    def full_name
      "#{first_name.capitalize} #{last_name.capitalize}"
    end
  end
  require "test_helper"

  class UserTest < ActiveSupport::TestCase
    def setup
      @user = User.new(first_name: "buffy", last_name: "summers")
    end

    test "returns the full name" do
      assert_equal "Buffy Summers", @user.full_name
    end
  end

What’s changed:

Since ActiveSupport::TestCase inherits from Minitest::Test, I can also mix and match syntaxes if I ever feel so inclined:

  require "test_helper"

  class UserTest < ActiveSupport::TestCase
    setup do
      @user = User.create(first_name: "buffy", last_name: "summers")
    end

    def test_returns_the_full_name
      assert_equal "buffy summers", @user.full_name
    end

    test "returns the user slug" do
      assert_equal "buffy_summers", @user.slug
    end
  end

Here, I use both Minitest syntax and the Rails DSL to define test examples in the same file. Does it work? Yep! Should I do it? I’d rather not!

But I wanted to show you that you can, even if you shouldn’t.

I did not even toggle minitest/spec which opens up a third syntax to set up and define your test examples. I’ll cover this in another post.

One thing I’m not showing here that you should know. In Rails, based on your type of test, you’ll want your test file to inherit from a different class:

Wrapping up

As always, I intended this post to be short, and failed.

If you’re starting out with Minitest and feeling a bit lost, I hope this post helped you understand the basics.

The TL;DR is:

Thank you to Cecile for her suggestions.