Model Tests Creating Test Fixtures and Automated Model Tests

In this demonstration, I will show how to create model tests, which are automated tests for verifying the correctness of model classes. As a part of this demo, I will also show how to create test fixtures, which are sample model objects that can be used in tests.

General Steps

In general, the steps for creating a model test are as follows.

Creating Fixtures and Test for the Todo Model Class

To demonstrate the steps for creating a model test, we will be building upon the todo base app by creating a model test for the existing Todo model class, depicted in Figure 1.

A class diagram depicting a Todo model class with the following attributes: a title string, description text, and a due date

Figure 1. The Todo model class.

In particular, we will add Todo test-fixture objects, and then, we will write a test that checks that objects are valid (i.e., that they do not contain any invalid data). This sort of test provides a good “sanity check”. For example, it would catch any syntax errors that were inadvertently introduced in the Todo model class code. If the model class were to contain attribute validations (to be introduced in upcoming demos), this test would also detect if an invalid attribute value was accidentally set in the test fixtures.

Base App Code

Step 1 Create Fixtures

Test fixtures are defined in the test/fixtures/ folder of the project. Each model class has a YAML file (e.g., todos.yml) in this folder that was generated by the rails generate model command that created the model class. The generated YAML files are populated with some crude examples, which we typically replace with something more sensible.

For example, the Todo fixtures in todos.yml initially look like this:

# == Schema Information
#
# Table name: todos
#
#  id          :bigint           not null, primary key
#  description :text
#  due_date    :date
#  title       :string
#  created_at  :datetime         not null
#  updated_at  :datetime         not null
#

one:
  title: MyString
  description: MyText
  due_date: 2021-01-23

two:
  title: MyString
  description: MyText
  due_date: 2021-01-23

To make the fixtures more realistic, we update them with attribute values that make more sense for a Todo object, like this:

# == Schema Information
#
# Table name: todos
#
#  id          :bigint           not null, primary key
#  description :text
#  due_date    :date
#  title       :string
#  created_at  :datetime         not null
#  updated_at  :datetime         not null
#

one:
  title: Slay the Dragon
  description: Go to the End and slay the Enderdragon.
  due_date: 2020-5-5

two:
  title: Do Homework
  description: Mrs. Dempsey says I have to do all my homework or she'll fail me.
  due_date: 2009-4-17

Note that, in the above YAML code, the strings, one and two, serve as IDs that can be used to retrieve a particular fixture. It is a common convention in Rails for the test fixtures of each model class to consist two fixtures, named one and two. However, sometimes more than two are needed for a particular test, and in those cases, more fixtures may be declared.

Test It!

There doesn’t appear to be a good way to test this step; however, any errors will become apparent when we test the subsequent steps below.

Step 1 Changeset

Step 2 Add an Empty Model Test

To begin our model test for the Todo model class, we declare an empty test in which the test logic will go. Each model generated using the rails generate model command is given a model-test class to hold tests related to the model. The model-test classes can be found in the test/models/ folder. Within that folder, there is a todo_test.rb file that contains the model-test class, TodoTest. We declare the empty test in this class, like this:

# == Schema Information
#
# Table name: todos
#
#  id          :bigint           not null, primary key
#  description :text
#  due_date    :date
#  title       :string
#  created_at  :datetime         not null
#  updated_at  :datetime         not null
#
require "test_helper"

class TodoTest < ActiveSupport::TestCase

  test "fixtures are valid" do
    
  end
  
end

Note that we opted to name the test with the string "fixtures are valid". We chose this particular name because the goal of the test is to verify that all the Todo test fixtures are valid. For each test declaration, Rails converts the declaration into a method. To name the method, Rails prefixes the name string with the word test and converts whole thing to snake_case. For example, the test we created above would be converted to a method named test_fixtures_are_valid. You will see the method name mentioned in the test output below.

Test It!

To confirm that we made this change correctly, we run the test we created, like this:

rails test -v

Technically, this command runs all the tests written for the web app, but so far, we have added only the one above. The -v option causes the command to give “verbose” (i.e., more-detailed) output.

The command should produce output similar to this:

Running via Spring preloader in process 66598
Run options: -v --seed 63759

# Running:

TodoTest#test_fixtures_are_valid = 0.08 s = .

Finished in 0.210694s, 4.7462 runs/s, 0.0000 assertions/s.
1 runs, 0 assertions, 0 failures, 0 errors, 0 skips

Note that output shows that the method for the test we wrote above, test_fixtures_are_valid, ran, and the output says, “1 runs”, which comes from this test. However, because our test contains no logic, we see zeros for all the other stats (i.e., “0 assertions, 0 failures, 0 errors, 0 skips”).

Step 2 Changeset

Step 3 Retrieve Fixture Objects

The aim of this test is to verify that all the test fixtures for the Todo model class are valid. We will need to retrieve each test fixture in order to test whether or not it’s valid. One way to retrieve the fixtures would be to use their IDs (one and two), like this:

class TodoTest < ActiveSupport::TestCase

  test "fixtures are valid" do
    todo_one = todos(:one)
    todo_two = todos(:two)
  end
  
end

Note that, for each fixture, Rails provides a fixture-retrieving method that is a snake_case, plural version of the model class name (e.g., todos). This name also happens to be the basename of the YAML fixture file (e.g., todos.yml). The fixture-retrieving method takes a symbol as its argument (e.g., todos(:one)), and will retrieve the fixture object with the corresponding ID. Thus, after the above code runs, the todo_one variable will reference a Todo object with attribute values corresponding to the fixture with ID one, and the todo_two variable will reference a Todo object with attribute values corresponding to the fixture with ID two.

Although the above code would work for our current test, it would need to be updated if, for example, more Todo fixtures were to be added or the names of the fixtures were to be changed. To make our test code more robust, we will instead iterate through all of the Todo fixtures, using the each method, like this:

class TodoTest < ActiveSupport::TestCase

  test "fixtures are valid" do
    todos.each do |todo|
      
    end
  end
  
end

The todos.each call will iterate through each of the Todo fixtures declared in todos.yml. The |todo| bit means that as each fixture object is visited, a variable todo will reference the current object. For now, we have left the body of this loop blank, to be filled in below.

Test It!

To confirm that we made this change correctly, we again run the test, using this command:

rails test -v

The output should be essentially the same as before, displaying one “1 runs” and “0 assertions, 0 failures, 0 errors, 0 skips”, because the test still contains no assertions to check whether the expected behavior was produced.

Step 3 Changeset

Step 4 Manipulate the Fixture Objects (if Needed)

For this particular test, there is no need to manipulate the fixture objects, because we want only to check if they are valid with respect to how they were defined in the fixtures YAML file. Thus, we can skip this step.

Step 5 Check for Expected Behavior

To check whether the retrieved fixture objects are valid, we use the model class method, valid?, to check whether each model object is valid, and we use the assert assertion method to make the test pass or fail, depending on the result returned by the valid? method, like this:

class TodoTest < ActiveSupport::TestCase

  test "fixtures are valid" do
    todos.each do |todo|
      assert todo.valid?
    end
  end
  
end

Note that each call to valid? made above will return the boolean value true if the object is valid or false if it’s not. Each call to assert will do nothing if the value returned by valid? is true (i.e., the assertion passed); however, if the value returned is false, the call to assert will log a test failure, displaying a failure message in the test output.

To make the failure message more meaningful, we add an additional argument to the assert call, like this:

class TodoTest < ActiveSupport::TestCase

  test "fixtures are valid" do
    todos.each do |todo|
      assert todo.valid?, todo.errors.full_messages.inspect
    end
  end
  
end

The second argument to assert adds additional information to any failure message produced by the call. In this case, the chain of calls, todo.errors.full_messages.inspect, use model class methods to return a string containing messages to help explain why the call to todo.valid? returned false.

Breaking down this chain of calls, the errors method returns the model object’s associated Errors object, which holds any attribute error messages for the model object. The full_messages method returns an array of all the error-message strings held in the Errors object. The call to inspect flattens the array of error-message strings into a single string, suitable for printing in the output of assert.

Test It!

To confirm that we made this change correctly, we again run the test, using this command:

rails test -v

This time, the final line of output should show that “2 assertions” ran, one for each iteration of the each loop above. All other parts of the output should be essentially the same as before.

Step 5 Changeset

Conclusion

Following the above steps, we have now added a model test that verifies that all the fixture objects are valid. Assuming that the test code is correct, if the test were to fail, it would tell us that we either made a mistake in our fixtures code or in or model class code.

Demo App Code