30 Sep 2021 · Software Engineering

    How to Test Rails Models with Minitest

    12 min read
    Contents

    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.

    Read also:

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    I'm a London based Ruby consultant. Visit my website for more information.