The use of Fixtures is a typical pattern in Rails applications because it is the framework’s default way of setting up test data. Fixtures seem harmless and helpful in the beginning, just like the Kevin, but as our codebase grows, the coupling introduced by fixtures makes our application increasingly challenging to maintain and extend. Let’s see how connascence as a coupling metric can build us arguments to avoid/remove Fixtures from our application.
Brief introduction to connascence
Connascence is a software metric and taxonomy for coupling. It introduces different types of coupling and metrics to give developers a vocabulary to talk about coupling. Strength, Locality, and Degree are used to estimate the impact of connascence.
Common use of Fixtures
Let’s take a look at these two fixtures files in isolation and see how much coupled they’re using connascence.
# spec/fixtures/users.yml matheus: id: 1 first_name: Matheus last_name: Sales role_id: 2 type: 2 # spec/fixtures/roles.yml admin: id: 1 name: admin user: id: 2 name: user
In this case, these simple files contain at least three types of connascence listed bellow from weakest to strongest.
- Connascence of Name: We will need to reference our user by the name
matheus, that’s not so bad because connascence of name it’s the weakest form of connascence.
- Connascence of Position: The user
matheusrole depends on the position of the
Rolefixtures because it specifies the
- Connascence of Meaning: We do not know what the magic value of
# spec/fixtures/users.yml matheus: first_name: Matheus last_name: Sales role: admin type: <%= User::B2C_TYPE %> # spec/fixtures/roles.yml admin: name: admin user: name: user
We’ve refactored our
users fixture to be dependent on the name instead of depending on the
of a specific
role. That’s a nice refactor because we’ve weakened our connascence
by moving from a connascence of position to name. And changing the
type field to use a constant,
we move from connascence of meaning to name. But how will this help us move away from fixtures?
Look at the big picture
Zero duplication is often a goal, and fixtures help on that side, but tests are one area where this is not ideal. We need to focus on treating our tests as documentation. And that’s where we need to think about why we are using fixtures. One of the biggest problems with this approach is sharing global data across all our test suite making the tests not self-contained and introducing the mystery guest testing antipattern.
Fixtures subtly affect the connascence of our tests, often without us realizing it. Let’s talk through an example:
def test_a_very_complicated_scope users = User.filter_by_very_complicated_scope assert_equal 2, users.size end
We have some problems with this test:
- Fragile test: any change on the fixture file may break this test and multiple ones.
- We have a nebulous test that doesn’t tell us a story and helps us understand what the object under testing means (connascence of meaning).
- We need to rely on our knowledge of the current fixtures to understand this test because the setup part of our test is implicit (connascence of meaning).
- The places where the test data is specified, and used are distant from each other. This is a locality problem (connascence of position).
def test_a_very_complicated_scope ## Setup admin = Role.create(name: "admin") user_role = Role.create(name: "user") user_admin_with_last_name = User.create(role: admin, first_name: 'Matheus', last_name: 'Sales') user_admin_without_last_name = User.create(role: admin, first_name: 'Matheus', last_name: '') user_without_last_name = User.create(role: user_role, first_name: 'Matheus', last_name: '') user_with_last_name = User.create(role: user_role, first_name: 'Matheus', last_name: 'Sales') ## Exercise users = User.filter_by_very_complicated_scope ## Verify assert_equal [user_admin_without_last_name], users end
The refactored version of our test does not use any pattern and makes our setup test phase explicit,
creating just enough data needed, and telling us a story that the
is related in some way to a user being of
admin role and not having a
last_name. This test is isolated from our test suite,
and does not depend on any shared global data.
But this approach might make our test challenging to read and more inconvenient to write. When dealing with objects that have multiple validations on different attributes, we need to pass multiple attributes just to make the object valid, most of these attributes may not be directly related to our test case.
Connascence is a way to measure coupling. Coupling makes code harder to be readable by humans, and without a human to read it, the code can’t be maintained, extended, or re-used. It can’t even be finished: the developer has to read the code to write it. A good test/suite needs to tell a story, and the usage of fixtures will make this a pretty hard thing to achieve and maintain. Factories will make your tests easier to read, better document your code, remove globally shared data, and reduce coupling and connascence.