Doubles are appropriate when you need a stand-in with a small amount of
functionality to isolate the code under test from its dependencies. But suppose
you were testing a new instance method of class Movie called name_with_rating
that returns a nicely formatted string
showing a movie’s title and rating. Clearly, such a method would have to access the title
and rating attributes of a Movie instance. You could create a double that knows all that
information, and pass that double:
But since the instance method being tested is part of the Movie class itself, it makes sense
to use a real object here, since this isn’t a case of isolating the test code from collaborator classes.
Where can we get a real Movie instance to use in such a test? Most testing frameworks for
object-oriented languages support the use of factories—bits of code (or declarative descriptions
of objects) framework designed to allow quick creation of full-featured objects (rather than
mocks) at testing time. The goal of a factory is to quickly create valid instances of a class
using some default attributes that you can selectively override for testing. For example, if
you were testing some code that allows a user to write a review for a movie, you might need a
valid movie instance to pass to that code. In the above scenario of testing a title-and-rating
formatter, you don’t care what the movie’s release date is, or who directed it; you just need a
movie object that is valid and whose title and rating you do know. So you would ask the factory
to produce a movie instance whose title and rating you specify, but whose other attributes you
don’t care about as long as they are valid values.
You might think this seems like more work than just creating a movie instance directly by
calling its constructor. In our simple example, that may be true. But there are two cases
in which factories really shine. The first is when the object to be created has many attributes
that must be initialized at creation time, even though any particular test case may only care
about the specific values of a few of them. For example, the app that manipulaties Movie objects
may have validations requiring a movie to have a valid release date or other fields meeting
specific criteria, yet the test above doesn’t care about the values of those other fields. In
such cases, you can ask the factory to create an object in which certain attribute values are
specified but others are filled in with valid defaults. The second case is when objects you
need to create have has-many or belongs-to relationships with other objects, as Chapter 5
describes. For example, if a Review belongs to a Movie, and you are creating a set of tests
to check various behaviors of Reviews, you literally cannot create a valid Review instance
without creating a Movie instance for it to belong to, even if the tests you are writing don’t
care about the movie itself. In this case, the Review factory can be configured so that creating
a Review also creates a valid Movie to which it belongs. Again, you can either specify a
particular Movie object you’ve created, or let the factory create one with valid default
values. Then in your test you can simply ask for a Review object to be created, without having
the details of the parent relationship clutter your test code.
The Ruby gem FactoryBot lets you define a factory for any kind of model in your app and
create just the objects you need quickly for each test, selectively overriding only certain
attributes, as Figure 8.8 shows.
# in spec/models/movie_spec.rbdescribeMoviedoit'should include rating and year in full name'do# 'build' creates but doesn't save object; 'create' also saves itmovie=FactoryBot.build(:movie,:title=>'Milk',:rating=>'R')expect(movie.name_with_rating).toeq'Milk (R)'endend
Figure 8.8: Using factories rather than fixtures preserves Independence among tests. Frameworks such as FactoryBot
(gem ’factory_bot_rails’ in Gemfile) make factory creation easy.
In database-backed MVC apps, one other source of “real” objects for use in tests is fixtures—a
set of objects whose existence is guaranteed and fixed, and can be assumed by all test cases.
The term fixture comes from the manufacturing world: a test fixture is a device that holds
or supports the item under test. Since all state in Rails SaaS apps is kept in the database,
a fixture file defines a set of objects that is automatically loaded into the test database
before
tests are run, so you can use those objects in your tests without first setting them up. Rails
looks for fixtures in a file containing objects expressed in YAML (a recursive acronym for
YAML Ain’t Markup Language), as Figure 8.9 shows. Following convention over configuration,
the fixtures for the Movie model are loaded from spec/fixtures/movies.yml, and are available
to your specs via their symbolic names, as Figure 8.9 shows.
# spec/models/movie_spec.rb:require'rails_helper.rb'describeMoviedofixtures:moviesit'includes rating and year in full name'domovie=movies(:milk_movie)expect(movie.name_with_rating).toeq('Milk (R)')endend
Figure 8.9: Fixtures declared in YAML files (top) are automatically loaded into the test database before each spec is
executed (bottom). After each example runs, the database is cleared out and the fixtures reloaded.
But unless used carefully, fixtures can interfere with tests being Independent, as every
test now depends implicitly on the fixture state, so changing the fixtures might change
the behavior of tests. In addition, although any given test probably relies on only one or
two fixtures, the union of fixtures required by all tests can become unwieldy. Therefore,
fixtures should be used very sparingly if at all, and primarily for truly fixed data that,
in production, would not be expected to change while the app is running but need to be present
in order for it to work. For example, at deployment time the app might allow setting the
timezone or language in which it operates and storing the preferences in the database, and
many aspects of the app might rely on these values being set to a legal value. Having a
fixture that “hardwires” some values suitable for testing is reasonable in this case. As a
rule of thumb, use factories for kinds of data that normally change while the app is running,
and consider fixtures for data that doesn’t change but must be present for the app to work at all.
Whether you use factories or fixtures, the test framework itself (in our case, RSpec) is
responsible for restoring the state of the world to look “pristine” before the next test
case runs, just as with doubles. Specifically, the database is completely erased, and any
fixtures are then reloaded. Doing this test teardown before every single example keeps tests
Independent.
Self-Check 8.6.1.Suppose a test suite contains a test that adds a model object to a table
and then expects to find a certain number of model objects in the table as a result. Explain
how the use of fixtures may affect the Independence of the tests in this suite, and how the
use of Factories can remedy this problem.
Figure 8.10: Some of the most useful RSpec methods introduced in this chapter. See the rspec.info documentation site for
details and additional methods not listed here.
Figure 8.11: Continuation of summary of useful RSpec methods introduced in this chapter.
If the fixtures file is ever changed so that the number of items initially populating that
table changes, this test may suddenly start failing because its assumptions about the initial
state of the table no longer hold. In contrast, a factory can be used to quickly create only
those objects needed for each test or example group on demand, so no test needs to depend
on any global “initial state” of the database.