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
matheus
role depends on the position of theRole
fixtures because it specifies therole_id
. - Connascence of Meaning: We do not know what the magic value of
type: 2
means.
# 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 id
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 filter_by_a_very_complicated_scope
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.
Most of the problems introduced by the usage of fixtures will be spread into our entire test suite. Fortunately, they can be fixed using a design pattern called Factory to generate the bare minimum data needed for that specific test —and sometimes factories aren’t even required to achieve this. Making each test independent and more straightforward is an excellent way to increase the maintainability of our codebases. This approach is quite common and many languages have libraries for writing tests with factories (for example, in JavaScript, Python, or Elixir).
Summary
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.