Eliminate hardcore nesting in your RSpec tests by using Ruby procs
Tags: ruby, rspec, testing
Published: 2022-07-18 10:14:27

---
title: Eliminate hardcore nesting in your RSpec tests by using Ruby procs
published: true
description: Learn how to define your test matrix using Ruby procs and decrease the nesting of your RSpec contexts. 
tags: ruby, rspec, testing
cover_image: https://images.unsplash.com/photo-1627453999380-da4f8e8d923c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1548&q=80
# Use a ratio of 100:42 for best results.
---

Recently I was testing a service class in Ruby and I needed to make sure that specific combinations of conditions produce desired effects.

First, I started nesting the conditions in contexts like I'd do normally, so my tests would look like this:
```ruby
context 'when A1' do
  let(:a1) {}

  it 'R1' do
    # ...
  end
  
  context 'and B1' do
    let(:b1) {}

    it 'R1' do
      # ...
    end
  end
  
  context 'and B2' do
    let(:b2) {}

    it 'R1' do
      # ...
    end
  end
end

context 'when A2' do
  let(:a2) {}

  it 'R2' do
    # ...
  end
  
  context 'and B1' do
    let(:b1) {}

    it 'R3' do
      # ...
    end
  end
  
  context 'and B2' do
    let(:b2) {}

    it 'R2' do
      # ...
    end
  end
end
```

In my case there was one more level of nesting, so the code was really unreadable and hard to understand. Here I removed one level and cleaned up the context contents to save your eyes dear reader :)

As you can see there was also a lot of repetition, so the first thing I did was extracting the repeating contexts into `shared_context` blocks and repeating test cases into `shared_examples`, so the code became similar to this:
```ruby
shared_context 'A1' do
  let(:a1) {}
end

shared_context 'A2' do
  let(:a2) {}
end

shared_context 'B1' do
  let(:b1) {}
end

shared_context 'B2' do
  let(:b2) {}
end

shared_examples 'R1' do
  it 'R1' do
    # ...
  end
end

shared_examples 'R2' do
  it 'R2' do
    # ...
  end
end

shared_examples 'R3' do
  it 'R3' do
    # ...
  end
end

context 'A1' do
  include_context 'A1'

  include_examples 'R1'
  
  context 'B1' do
    include_context 'B1'

    include_examples 'R1'
  end
  
  context 'B2' do
    include_context 'B2'

    include_examples 'R1'
  end
end

context 'A2' do
  include_context 'A2'

  include_examples 'R2'
  
  context 'B1' do
    include_context 'B1'

    include_examples 'R3'
  end
  
  context 'B2' do
    include_context 'B2'

    include_examples 'R2'
  end
end
```

Ok, at least now, whenever the details of a test or context case change I wouldn't have to update all the places.

However, the tests would still be difficult to read due to nesting, so I thought that it might be cool to define each scenario in a single line instead of having to browse through endless nested blocks.

Imagine something like in the snippet below. I translated the nested contexts and examples from previous code block, so that in each line the last element of the array defines the expected `shared_examples` group and all the previous elements are names of `shared_context`s that need to be deeply nested one after another:
```ruby
[
  ['A1', 'R1'],
  ['A1', 'B1', 'R1'],
  ['A1', 'B2', 'R1'],
  ['A2', 'R2'],
  ['A2', 'B1', 'R3'],
  ['A2', 'B2', 'R2'],
].each do |scenario|
  # ... do some Ruby + rspec magic here
end
```

Reads much better, right? Let's see how the full solution looks like:
```ruby

[
  ['A1', 'R1'],
  ['A1', 'B1', 'R1'],
  ['A1', 'B2', 'R1'],
  ['A2', 'R2'],
  ['A2', 'B1', 'R3'],
  ['A2', 'B2', 'R2'],
].each do |scenario|
  # All but last element of the array
  contexts = scenario[0..-2]
  test_example = scenario.last

  # Let's prepare a deeply nested set of Procs with all the required nesting to achieve the same result. 
  # A tricky thing to understand here is that we need to start from the most nested block and go up,
  # until the most outer context.
  contexts.reverse.inject(proc { include_examples(test_example) }) do |inner, ctx|
    proc do
      context ctx do
        include_context(ctx, &inner)
      end
    end
  end.call
end
```

I understand that this solution might be very specific to the problem I had, but perhaps it might inspire you to refactor your tests and improve their readability.

If you know a simpler or better method (a Ruby gem? Built-in RSpec functionality?) of dealing with such problems please let me know in a comment or private message.