Creating and Testing Valid Fixtures
In this demonstration, I will show how to create fixtures (i.e., sample data) for model tests and how to write tests to verify that all the fixtures for a particular model are valid. We will continue to build upon the QuizMe project from the previous demos.
Unit tests aim to verify that individual “units” of code (e.g., classes and methods) work correctly. Rails includes a test harness that provides automation to help developers to write tests for various units of Rails application code (e.g., model classes) and to run those test in batches. In this demo, we will use the Rails test harness to unit test our McQuestions
model class.
To accomplish this goal, we will perform the following steps:
- Remove some controller test code that was previously generated by a
rails g controller
command. This code became broken because of customizations we made (e.g., to theroutes.rb
file). - Create a couple
McQuestion
test fixtures to use in our model tests. - Write unit tests that verify that our
McQuestion
class is free of syntax errors and that ourMcQuestion
fixtures are all valid.
1. Cleaning Up Generated McQuestionsController
Tests
When rails
is used to generate a controller class (like we did in previous demos), it also generates some default tests for the controller class, including some “controller tests”; however, the controller tests assume the default routes generated by rails
. When we customize the routes (as we are apt to do), it breaks these controller tests. Thus, to solve this problem, we commonly just comment out the generated controller tests initially, and later update them to fit for our particular app.
To remove the generated controller tests, use VS Code to edit the Ruby files in test/controllers
, and comment out any tests there that you yourself did not write (all of them at this stage). In a later demo, we will write controller tests specifically designed for the QuizMe project.
2. Creating Valid McQuestion
Fixtures
In Rails, the fixtures for each model are stored in a YAML (.yml
) file in the test/fixtures
directory. Looking at the mc_questions.yml
file, we can see that a couple of default fixtures were generated automatically, named one
and two
. We will edit those two fixtures to give them attribute values more germane to multiple-choice questions. These fixtures are to all contain valid McQuestion
sample data (no blank or missing questions, no blank or missing answers, etc.).
Replace fixture one
with the following:
one:
question: By default, every Rails model is a subclass of which superclass?
answer: ApplicationRecord
distractor_1: Object
distractor_2: ActiveModel
True/false questions are another valid kind of multiple-choice question. Replace fixture two
with the following:
two:
question: The command rails db:migrate updates the schema.rb file.
answer: true
distractor_1: false
distractor_2: # blank loads as nil
Once the above changes are completed, every McQuestion
model test will be able to retrieve these fixtures for use in their test code.
3. Testing the Valid McQuestion
Fixtures
Now that we have some fixtures, we will write a test that checks the fixtures to make sure that the app also sees them as valid. If the test fails, then we either accidentally introduced a bug into our model code or made a mistake in the fixture, and we will have to locate and fix the bug.
When we used rails
to generate the McQuestion
model, rails
also generated a test file test/models/mc_question_test.rb
. That test file is where we should put any model tests we write for the McQuestion
class.
Rails model tests usually follow three basic steps:
- First, the test retrieves one or more fixtures (i.e., valid model objects for testing).
- Next, the test may (or may not, depending on what it’s testing) set the attribute values of the fixture objects, commonly to make them invalid.
- Finally, the test makes one or more assertions about the fixtures. Each assertion aims to check that some condition is true. If all of a test’s assertions are true, then the test passes; however, if an assertion is discovered to be false, then the test fails.
Rails model tests are considered a kind of unit test, so they should be small and focus on testing only one thing. However, you can check multiple details about that one thing by adding multiple assertions to the same test.
3.1. Creating a Test for a Single Valid McQuestion
Fixture
Although our ultimate goal is to test all the McQuestion
fixtures, as a first step, we will write a test that tests only one fixture, one
, to verify that the system considers it valid.
In the body of the McQuestionTest
class, insert an empty test with the name “fixtures are valid
”, like this:
test "fixtures are valid" do
# TODO
end
Next, retrieve the test fixture one
object by inserting a line of code, like this:
test "fixtures are valid" do
q = mc_questions(:one)
# TODO
end
Rails automatically provides the mc_questions
method (same name as fixture’s YAML file). When called, as above, with a symbol argument (:one
) corresponding to the name of a test in the YAML file, the method will return the associated fixture object from the database.
Finally, check that the object is valid by inserting an assert
statement, like this:
test "fixtures are valid" do
q = mc_questions(:one)
assert q.valid?, q.errors.full_messages.inspect
end
Now, when this test executes, if the call to the valid?
method returns false
, indicating that the McQuestion
object referenced by q
is not valid, then the test will fail, printing error messages to the console.
Note that, for this test, we skipped step 2 mentioned above (setting fixture attributes), because this test doesn’t need to change any of the fixture’s attribute values.
Make sure that the test passes to confirm that everything is working by entering the following command:
rails test
You should see an output message that looks like this, indicating that all tests passed:
Finished in 0.126389s, 7.9121 runs/s, 7.9121 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
The number of runs
indicates how many test
methods executed. The number of assertions
indicates how many assertions ran within those test
methods. The number of failures
indicates many of those assertions were violated. The number of errors
indicates how many test
methods crashed (e.g., due to syntax errors or thrown exceptions in the code under test).
3.2. Updating the Test to Cover All the Valid McQuestion
Fixtures
Now that we have a test to verify that McQuestion
fixture one
is valid, what we would really like to do is to test that all of the McQuestion
fixtures are valid. To do so, we could copy/paste/modify the code we already have to add a second assertion for fixture two
, but if we do that, we’re setting ourselves up to do a copy/paste/modify for every fixture we want to test. Right now, there are only two fixtures, but we might add more later. To save us from tedious and error-prone copy/paste/modify edits, we will instead simply iterate through all the fixtures and assert that each one is valid. Thus, we will update the above test code as follows.
To begin with, remove the previous body of the test to give us a clean starting place, like this:
test "fixtures are valid" do
# TODO
end
Next, iterate through all the McQuestion
fixtures by inserting an each
loop, like this:
test "fixtures are valid" do
mc_questions.each do |q|
# TODO
end
end
Note that when called with no arguments, the mc_questions
method returns a collection of all the McQuestion
fixture objects.
Finally, assert that each McQuestion
object is valid by inserting an assert
statement similar to the one above, like this:
test "fixtures are valid" do
mc_questions.each do |q|
assert q.valid?, q.errors.full_messages.inspect
end
end
Using the same rails
command as above, re-run the tests to verify that they all pass and that everything is working properly. Note that the test output now shows that 2 assertions
executed, one for each of our McQuestion
fixtures.
We now have an automated test that we can run again and again to ensure that we haven’t introduced any errors into our McQuestion
model code or our McQuestion
fixtures.
For more information on fixtures, see the Rails Guides page and API documentation.