Using a Dependency Graph to Visualize RSpec let

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?

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

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

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

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

Follow the dependencies

When a let is invoked, its block might reference other lets, 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

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

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

dependency graph of lets for the it is an active product test with all executed nodes highlighted


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 lets
  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.