[pt-BR] Existe uma versão desse artigo em pt-BR, que você pode acessar aqui
In recent years, much of my work has involved helping teams expand and optimize their automated test suites. And one thing that comes up repeatedly is the same kind of problem: the tests exist, but they are hard to understand.
No one knows exactly what is being tested, where certain scenarios are, or whether the logical branches are truly covered. As a result, you see coverage gaps, redundant tests, and a lot of time wasted trying to understand what already exists.
On many occasions, when pairing with less experienced developers, I notice a recurring pattern: they spend a long time scrolling up and down the test file, looking for code that looks “similar” to what they want to write. The idea is to place the new test “near the other similar ones.” This habit is understandable, but it’s also a symptom of a structural problem: if you need to “guess” where the test should go, the file structure isn’t clear enough.
Over time, I developed a technique that helps me organize and visualize test structures. It’s simple and practical, and it leverages a feature that almost every modern text editor has: code folding.
I call this technique Unfolding Tests.
The examples in this article use Ruby on Rails and RSpec, but the same principles apply to any other testing framework.
The idea behind Unfolding Tests
The idea is to use code block folding as both a reading and writing tool. In VS Code, for example, you can fold and unfold blocks such as describe
, context
, and it
.
To start, use the Fold All command (on macOS: Cmd + Shift + P; on Windows: Ctrl + Shift + P) and type “Fold All.”
This technique is grounded in the idea of reducing cognitive load. When everything is folded, you see only the skeleton of the test, which lets you quickly understand what’s being tested and how the scenarios are organized, without reading the implementation details. That way, the focus becomes the intent, rather than every line of code.
From there, I gradually expand the blocks and come to understand:
- What’s being tested (
describe
) - What the possible scenarios are (
context
) - What the expectations are in each scenario (
it
) - And which setups are used at each level
This hierarchical view provides a much clearer reading of the system’s behavior under test. What’s most interesting is that the same process also works when writing tests.
Writing tests
When I start a new test file, I don’t think in lines of code. I think in the structure I want to see when folded.
First, I create the skeleton of the test:
RSpec.describe UserPolicy do
context 'when user is admin' do
it 'allows access'
end
context 'when user is not admin' do
it 'denies access'
end
end
When that file is fully folded, what I see is something like:
UserPolicy
when user is admin
allows access
when user is not admin
denies access
This is already enough to understand what’s being tested and what the scenarios are.
By the way, this view is very similar to the RSpec output when you use the --format documentation option.
If you’re not using that format yet, you should. It turns test execution into an almost narrative reading, showing exactly what each test describes, in the same hierarchy you defined in the
describe
,context
, andit
blocks.
After that, I unfold block by block and fill in the details: setups, mocks, expectations, and so on.
The hierarchy of setups
One of the most important parts of Unfolding Tests is respecting the hierarchy between the blocks and their setups.
- Under each
describe
, there should be a setup with the general prerequisites for all scenarios inside that block. - Under each
context
, there should be a setup with the prerequisites specific to that scenario. Your setup here must provide the conditions that make thecontext
description true. - The test code itself should always be inside the
it
block, which is the lowest level in the structure where expectations are verified.
describe 'Listing orders' do
let(:admin_user) { create(:user, :admin) }
before do
sign_in(admin_user)
visit orders_path
end
context 'when there are no orders' do
before { Order.delete_all }
it 'shows the empty state message' do
expect(page).to have_content('No orders found')
end
end
context 'when there are orders' do
let!(:orders) { create_list(:order, 2) }
it 'lists all orders' do
expect(page)
.to have_text(orders.first.id)
.and have_text(orders.second.id)
end
end
end
If you fold this file, even without seeing the implementation details, you can understand the purpose of the test, the scenarios it covers, and the expected behavior in each case. Then, when you unfold each block, you reveal the necessary details: first the setup, then the test.
Contexts as logical branches
An important principle of this technique is that each logical branch deserves its own context
. If a variable can take three states (for example, full
, partial
, empty
), then you should have three contexts, one for each case.
Example:
context 'when report is full' do
# ...
end
context 'when report is partial' do
# ...
end
context 'when report is empty' do
# ...
end
This helps ensure that the tests truly explore all relevant possibilities.
And more: to me, it doesn’t make sense to have a context without others that represent opposing states. If there’s a context 'when user is admin'
, there should also be a context 'when user is not admin'
. These pairs make it clear that the test covers both sides of the logic, and that’s easy to see when the file is folded.
Why this works
Unfolding Tests turns the act of reading and writing tests into something visual and incremental.
You see the structure before you see the details, which helps you:
- Quickly understand the scope of the file
- Identify redundancies and gaps
- Ensure that each logical path has its counterpart
- Write tests that also serve as living documentation
In the end, a good test is also a good form of documentation. And the clearer the intent expressed in the structure, the easier it will be for anyone to understand the expected behavior of the system.
Tips to start applying
Fold everything (
Fold All
).
Start with the test file completely folded. This gives you a clear overview of the structure, the main blocks, and the existing scenarios.Unfold gradually.
Expand the outerdescribe
blocks first to understand what’s being tested. Then open thecontext
blocks to see the scenarios, and finally theit
blocks to look at the expectations. This gradual progression helps you understand the test layer by layer, without overloading your mind with unnecessary details right away.Check if the structure makes sense when folded.
If, with everything folded, you can’t tell what the file is testing or which scenarios it covers, that’s a sign the structure needs to be revisited.When writing new tests, think about the structure first.
Build the skeleton withdescribe
,context
, andit
before writing any code.Use
context
to express logical variations.
Whenever possible, write contexts in pairs — for example, “when user is admin” and “when user is not admin.”Respect the setup hierarchy.
Place generalbefore
andlet
definitions inside thedescribe
, and specific setups inside eachcontext
.Only then write the test code (
it
).
Once the structure and prerequisites are clear, add the actual test code.
Unfolding Tests is a simple way to bring clarity and intent to your tests.
It forces you to think about structure before details and to treat each scenario as part of a cohesive whole. In the end, the benefit is twofold: tests become easier to understand, and the code is less likely to get lost among confusing or redundant cases.
If you often get lost in large test files, try folding everything and then unfolding gradually. It’s a very simple technique, yet it’s impressive how much it changes the way you read and write tests.
Top comments (0)