---
title: Using a Dependency Graph to Visualize RSpec let
teaser: A visual answer to the question "what data is created when I execute this
  test?".
tags: development,web,ruby,rails,testing
author: Joël Quenneville
published_on: 2022-10-03
---

What data is created when I execute this RSpec test? This question is not as
straightforward as you might think. Spec files that rely on `let` often spread
these declarations all over the file so you have to scroll around to find them.
Additionally, `let` is _lazy_ so a particular declaration won't actually be
executed unless it is referenced, either directly by your test or indirectly via
one of the direct references. And just to make things even harder, there is also
`let!` which is _eager_ and always executes no matter what! 😱

In these situations, I like to use a visual approach with a dependency graph to
understand what is going on.

## Our test

Consider a test file like this. It has a mix of `let` and `let!` defined at
various places in the file and at various levels of nesting. What data is
created for the `it "is complete"` and `it "is an active product"` tests?

```ruby
describe User do
  let(:organization) { create(:organization) }
  let(:user) { create(:user, organization: organization) }
  let!(:admin) { create(:user, admin: true, organization: organization) }

  # lots of other tests

  describe "Invoices" do
    let(:product) { create(:product) }
    let(:invoice) { create(:invoice, owner: user, items: [product] }

    it "is complete" do
      expect(invoice).to be_complete
    end

    it "is an active product" do
      expect(product).to be_active
    end
  end
end
```

## List the lets

The first step is to list the `let` declarations in our file. Start near your
test and work outward. This lets us know what we are working with. In our
example we have:

- invoice
- product
- admin
- user
- organization

## Draw connections

Next, go through the list and see if the associated block references any other
`let` declarations. For example `let(:user) { create(:user, organization:
organization) }` references the organization `let`. If they do, draw an arrow
pointing to that reference (in this case from user to organization). You should
end up with a diagram that looks like this.

![dependency graph of lets in a spec file](https://images.thoughtbot.com/main/JveklAVGTziFKxRkqDcR_let-graph.png)

## Mark the eager values

Now we can start marking which items get executed. Let's shade them orange.

`let!` is eager - it will always be executed regardless of whether it is
referenced or not. Since `admin` is defined with `let!`, we can immediately
shade it without looking at the individual tests.

![dependency graph of lets with the let! values highlighted](https://images.thoughtbot.com/main/0TS3JQJ2QpKrC8AQ6fod_let-bang.png)

## Follow the dependencies

When a `let` is invoked, its block might reference other `let`s, causing them to
be evaluated as well. To account for this, we find all of our shaded boxes,
follow the _outbound_ arrows, and shade all dependencies. Keep doing this until
we've gone as far as we can go.

It's important not to follow arrows in reverse! Here we follow the arrow from
the admin to the organization and shade it. We can't go backwards from the
organization to the user.

![dependency graph of lets with let! and everything downstream highlighted](https://images.thoughtbot.com/main/5T5aTJTORqGHm6t7lpYP_let-bang-dependencies.png)

## Follow lazy dependencies from tests

Now we can actually get to the individual tests. For each test, mark any `let`
referenced directly in the test. Then, as before, follow the outbound arrows as
far as they will go, marking every box.

For the test `it "is complete"`, the final diagram looks like this. Every box is
shaded so all of these lets will trigger. Perhaps that's more than you expected?
Does this test really need a product?

![dependency graph of lets for the it is complete test with all executed nodes highlighted](https://images.thoughtbot.com/main/vsIPK2KTJe6RfGnO1Qf1_let-it-is-complete.png)

For the test `it "is an active product"`, the diagram looks like this. You can
see that not all of our `let`s get executed this time.

![dependency graph of lets for the it is an active product test with all executed nodes highlighted](https://images.thoughtbot.com/main/FBijRo9jRzCqW4musJSd_let-it-is-an-active-product.png)

## Summary

If we streamline the process a bit by merging the two "follow the dependencies"
steps together, we end up with the following 5 steps:

1. List the `let`s
2. Draw connections
3. Mark eager values
4. Mark values in tests
5. Follow the dependency arrows

Using this visual approach, I find it much easier to understand what data is and
isn't created in large, gnarly spec files. It can also be a good teaching tool
for understanding the nuances between `let` and `let!`.

If all this complexity with `let` is frustrating for you, check out [Let's Not]
and [The Self-Contained Test] for a radically different approach to writing
tests that avoids `let` altogether.

[Let's Not]: https://thoughtbot.com/blog/lets-not
[The Self-Contained Test]: https://thoughtbot.com/blog/the-self-contained-test
