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.
-
Step 1 Create Fixtures. This step creates some sample model objects (test fixtures) that can be retrieved and used in model tests.
-
Step 2 Add an Empty Model Test. This step declares a test method in which the code that performs the model test will go.
-
Step 3 Retrieve Fixture Objects. This step adds code to the test method that retrieves one or more test-fixture objects to be used in the test.
-
Step 4 Manipulate the Fixture Objects (if Needed). Sometimes it is necessary to manipulate the retrieved fixture objects in order to set up the test. For example, the value of a fixture object’s attribute might need to be set. This step performs such manipulations on the fixture objects.
-
Step 5 Check for Expected Behavior. This step adds code to the model test that checks whether the model object(s) exhibited the correct behavior. This code typically involves one or more assertion statements. An assertion statement evaluates an object or expression to check if an expected result was produced. If a different result than the one expected is produced, then the test fails, revealing a defect.
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.
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.
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 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 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 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.
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.