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:

  1. 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 the routes.rb file).
  2. Create a couple McQuestion test fixtures to use in our model tests.
  3. Write unit tests that verify that our McQuestion class is free of syntax errors and that our McQuestion 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.

Code changeset for this part

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.

Code changeset for this part

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:

  1. First, the test retrieves one or more fixtures (i.e., valid model objects for testing).
  2. 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.
  3. 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).

Code changeset for this part

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.

Code changeset for this part