How to test rails models with minitest

How to Test Rails Models with Minitest

Learn how to test the main aspects of Ruby on Rails models with the Minitest testing suite.

Cut your Rails test suite down to a few minutes with one-click automatic parallelization.

Automate parallelizing tests

Introduction

In this tutorial, we will cover how to test Ruby on Rails models with the Minitest testing suite. After completing this tutorial, you will have learned the following:

  • What to test in Rails models,
  • How to test the various aspects of a Rails model, such as validations, associations, scopes, and extra business logic methods, and
  • How to use fixtures to refactor and simplify your model tests.

We will be using Ruby 2.3, Rails 5.0 and Minitest 5.8. However, older versions should have the same behavior. The only difference to note is that models in Rails 5.0 inherit from ApplicationRecord instead of ActiveRecord::Base.

You will need a working install of Ruby and Ruby on Rails. You don't need to have any existing knowledge of Minitest, however, we do have a tutorial on Getting Started with Minitest.

At the end of this tutorial, you will have a Rails application with a fully tested user model.

What is Minitest?

Minitest is a testing suite for Ruby. It provides a complete suite of testing facilities supporting test-driven development (TDD), behavior-driven development (BDD), mocking, and benchmarking. It's small, fast, and it aims to make tests clean and readable.

Minitest is the default testing suite used with Rails, so no further setup is required to get it to work. Along with RSpec, it is one of the two most commonly used testing suites used in Ruby.

If you would like to learn more about RSpec, we have a tutorial on Getting Started with RSpec, as well as tutorial on Testing Rails Models with RSpec.

Rails Models

In Ruby on Rails, models are Ruby classes whose primary purpose is to talk to the database via ActiveRecord. They also provide the functionality to validate data, and sometimes they also contain business logic.

Getting Started

Assuming you already have Ruby and Ruby on Rails set up, we can start by creating our Rails application.

rails new testing-rails-models

Adding Our Model

For this tutorial, we will be using a user model as an example. The data attributes we want to store are name and email. We can use Rails to generate this model, along with a database migration, test file and fixtures.

bin/rails generate model User name:string email:string
Running via Spring preloader in process 15276
      invoke  active_record
      create    db/migrate/20160708114436_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml

We need to run the database migration before proceeding.

bin/rake db:migrate
Running via Spring preloader in process 15424
== 20160708114436 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0021s
== 20160708114436 CreateUsers: migrated (0.0022s) =============================

Now, we are all set up and ready to write our first tests.

Validation Tests

In the spirit of TDD, we will add our tests first, and then write code to make those tests pass. Our requirements are that a user needs to have a name and an email.

Let's add a skeleton for these tests.

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

class UserTest < ActiveSupport::TestCase
  test 'valid user' do
  end

  test 'invalid without name' do
  end

  test 'invalid without email' do
  end
end

To make sure we are on the right path, let's run our test.

bin/rake
Running via Spring preloader in process 16856
Run options: --seed 50464

# Running:

...

Finished in 0.025017s, 199.8669 runs/s, 0.0000 assertions/s.

3 runs, 0 assertions, 0 failures, 0 errors, 0 skips

Note that three tests were run but zero assertions were made. It's now time to add the assertions.

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

class UserTest < ActiveSupport::TestCase
  test 'valid user' do
    user = User.new(name: 'John', email: 'john@example.com')
    assert user.valid?
  end

  test 'invalid without name' do
    user = User.new(email: 'john@example.com')
    refute user.valid?, 'user is valid without a name'
    assert_not_nil user.errors[:name], 'no validation error for name present'
  end

  test 'invalid without email' do
    user = User.new(name: 'John')
    refute user.valid?
    assert_not_nil user.errors[:email]
  end
end

We've checked if the record is valid, but it is necessary to check if the correct error is present as well. Otherwise we cannot be sure that the correct validation is making our record invalid.

Let's run the tests again and see what we need to do next.

bin/rake
Running via Spring preloader in process 19743
Run options: --seed 13205

# Running:

F

Failure:
UserTest#test_invalid_without_name [/.../testing-rails-models/test/models/user_test.rb:11]:
user is valid without a name


bin/rails test test/models/user_test.rb:9

F

Failure:
UserTest#test_invalid_without_email [/.../testing-rails-models/test/models/user_test.rb:17]:
Expected true to not be truthy.


bin/rails test test/models/user_test.rb:15

.

Finished in 0.046156s, 64.9966 runs/s, 64.9966 assertions/s.

3 runs, 3 assertions, 2 failures, 0 errors, 0 skips

The test for a valid object should pass, while the other tests should fail since we have still not added any validations. Bear in mind that the order of the tests is random every time, depending on the random seed value.

It's worth pointing out that assertions take a final argument, a string which can provide a helpful failure message about why the test failed. These are optional, but can be helpful. Notice the difference in the output above.

Adding Validations to Make Tests Pass

Let's take a look at our model and add the required validations to make our tests pass.

# app/models/user.rb
class User < ApplicationRecord
  validates :name, :email, presence: true
end

Note that User inherits from ApplicationRecord in Rails 5.0, but in previous versions it would inherit from ActiveRecord::Base.

Now let's see if our tests pass.

bin/rake
Running via Spring preloader in process 19918
Run options: --seed 5832

# Running:

...

Finished in 0.038829s, 77.2612 runs/s, 128.7687 assertions/s.

3 runs, 5 assertions, 0 failures, 0 errors, 0 skips

All of our tests have passed, but there is some duplication in them.

Refactoring Tests

The great thing about Minitest is that tests are just normal Ruby code. As a result, tests can be refactored like any other code in Ruby.

Minitest by default runs a setup method in every test class before each test is run. By defining this method in our test we can do any necessary setting up for our test in one place.

Let's take advantage of this and create our user object in setup for use in our tests.

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

class UserTest < ActiveSupport::TestCase
  def setup
    @user = User.new(name: 'John', email: 'john@example.com')
  end

  test 'valid user' do
    assert @user.valid?
  end

  test 'invalid without name' do
    @user.name = nil
    refute @user.valid?, 'saved user without a name'
    assert_not_nil @user.errors[:name], 'no validation error for name present'
  end

  test 'invalid without email' do
    @user.email = nil
    refute @user.valid?
    assert_not_nil @user.errors[:email]
  end
end

Fixtures

Remember the fixture that Rails automatically generated for us earlier? It can help separate data from our tests.

An alternative to using fixtures is to use factories. If you want to know more about them, you can take a look at our tutorial on Working Effectively with Data Factories Using FactoryGirl.

Instead of declaring the user data in our tests, let's declare them in a fixture.

# test/fixtures/users.yml
valid:
  name: John
  email: john@example.com

Then, we should load it in our test's setup method.

# test/models/user_test.rb
def setup
  @user = users(:valid)
end

Now, run rake again to double check that our tests are still passing.

Associations

Our users are supposed to be able to create posts which have a title and a body. To accomplish this, we need to define an association.

Let's start by creating a test for this behavior.

# test/models/user_test.rb
test '#posts' do
  assert_equal 2, @user.posts.size
end

Run rake, and the new test will fail. To keep things short, the output has been trimmed down to show only the error.

bin/rake

Error:
UserTest#test_#posts:
NoMethodError: undefined method `posts' for #<User:0x007ffbe2c8c8d8>
    test/models/user_test.rb:25:in `block in <class:UserTest>'

We could fix this error by adding a method called posts to the User model. However, we want the association from ActiveRecord to do that for us. To fix this, we must first create the model for Post:

bin/rails generate model Post title:string body:text user:belongs_to
Running via Spring preloader in process 42550
      invoke  active_record
      create    db/migrate/20160711202244_create_posts.rb
      create    app/models/post.rb
      invoke    test_unit
      create      test/models/post_test.rb
      create      test/fixtures/posts.yml

Run the migration that was generated for us:

bin/rake db:migrate
Running via Spring preloader in process 43210
== 20160711202244 CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.0248s
== 20160711202244 CreatePosts: migrated (0.0249s) =============================

Now that we have the Post model set up, we can add the association for users to have posts:

# app/models/user.rb
class User < ApplicationRecord
  has_many :posts
  validates :name, :email, presence: true
end

Let's run our tests again. We should get a new error which tells us what to do next:

bin/rake

Failure:
UserTest#test_#posts [/.../testing-rails-models/test/models/user_test.rb:25]:
Expected: 2
  Actual: 0

We can see the association is working, but the number of associated posts is not the one we've expected. We need to have the correct fixtures to make our test pass.

Rails automatically created two posts fixtures for us. We'll keep them as they are, except for changing the user to valid which is our valid user fixture from before.

# test/fixtures/posts.yml
one:
  title: MyString
  body: MyText
  user: valid

two:
  title: MyString
  body: MyText
  user: valid

Now, run rake and the tests should pass.

bin/rake
Running via Spring preloader in process 44035
Run options: --seed 20896

# Running:

....

Finished in 0.071725s, 55.7685 runs/s, 83.6528 assertions/s.

4 runs, 6 assertions, 0 failures, 0 errors, 0 skips

Scopes

We want to be able to see a list of recent users, i.e. the users created within the previous week. Let's add a test for this.

# test/models/user_test.rb
test '#recent' do
  assert_includes User.recent, users(:valid)
  refute_includes User.recent, users(:old)
end

We also need to add a fixture for an old user to have something to test against.

# test/fixtures/users.yml
old:
  name: Old
  email: old@example.com
  created_at: <%= 2.weeks.ago %>

By writing our test to make sure that the valid user is included and the old user is not, we make sure we do not break the test when we add another user fixture in the future.

Run the tests, and they should fail.

bin/rake
Error:
UserTest#test_#recent:
NoMethodError: undefined method `recent' for #<Class:0x007ffbe5858278>
    test/models/user_test.rb:29:in `block in <class:UserTest>'

Now, we can add the scope.

# app/models/user.rb
class User < ApplicationRecord
  scope :recent, -> { where('created_at > ?', 1.week.ago) }
  has_many :posts
  validates :name, :email, presence: true
end

Now that we have added the scope, our tests should pass again.

Business Logic

We want to be able to generate a URL to a user's profile photo using Gravatar. To do this, we have to generate an MD5 encoded hash of the user's email and plug it into a URL.

We can use this page provided by Gravatar to generate a URL for our valid user to use in our test by entering john@example.com in the form. Note that any extra parameters are omitted since they are not needed.

# test/models/user_test.rb
test '#profile_photo_url' do
  assert_equal(
    'https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6',
    @user.profile_photo_url
  )
end

If we run our tests then our new test should fail.

bin/rake
Error:
UserTest#test_#profile_photo_url:
NoMethodError: undefined method `profile_photo_url' for #<User:0x007ffbe19b6bc8>
    test/models/user_test.rb:36:in `block in <class:UserTest>'

Let's add our first iteration of the #profile_photo_url method to make these tests pass.

# app/models/user.rb
def profile_photo_url
  "https://s.gravatar.com/avatar/#{Digest::MD5.hexdigest(email)}"
end

Our new test should pass, though this code can still be improved. The hashing of the email should be its own private method, the URL should come form a constant, and we should use a URI builder to build our URL.

Let's fix this.

# app/models/user.rb
class User < ApplicationRecord
  scope :recent, -> { where('created_at > ?', 1.week.ago) }

  PROFILE_PHOTO_ROOT_URL = URI 'https://s.gravatar.com/avatar/'

  has_many :posts
  validates :name, :email, presence: true

  def profile_photo_url
    url = PROFILE_PHOTO_ROOT_URL.clone
    url.path << email_md5
    url.to_s
  end

  private

  def email_md5
    Digest::MD5.hexdigest email
  end
end

Make sure to re-run the tests after refactoring, everything should still pass.

We do not explicitly test private methods as it is not necessary to do so unless you have a good reason to.

Ideally, all the code to generate our URL should live in a standalone, reusable service class, for example GravatarService. It should have its own tests and User#profile_photo_url should call it.

Conclusion

In this tutorial, we have covered how to test Rails models with Minitest from validations, scopes, associations, to extra business logic methods.

If you found this tutorial useful, you might want to check out the many other tutorials we have on testing in the Semaphore Community. We'd also like to hear your comments and questions, so feel free to leave them in the section below.

P.S. Would you like to learn how to build sustainable Rails apps and ship more often? We've recently published an ebook covering just that — "Rails Testing Handbook". Learn more and download a free copy.

51cee7009c304faca416b4475bd42446
Heidar Bernhardsson

I'm a London based Ruby consultant. Visit my website for more information.

on this tutorial so far.
User deleted author {{comment.createdAt}}

Edited on {{comment.updatedAt}}

Cancel

Sign In You must be logged in to comment.