DRY up RSpec subject defining
Tags: rspec, tests, dry
Published: 2023-09-27 15:19:12

---
title: DRY up RSpec subject defining
published: true
description: 
tags: RSpec, tests, dry
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-09-25 11:04 +0000
---

Immediate caveat - usually you want to favour KISS over DRY in specs. I've [railed against shared examples](https://dev.to/epigene/the-case-against-shared-examples-39kh) (a DRYing technique) in the past, so this may seem hypocritical of me, but hear me out.

I'll argue that there is a difference between "boilerplate" code (the `describe`, `context` and `it` calls) and "substance" code (`let`s and actual asserts), and that reducing the boilerplate will make the substance shine through better.

## The example

Let's look at a naive example. We have a class that has a class method, an initializer, and an instance method:

```rb
class Triangle
  attr_reader :a, :b, :c

  def initialize(a, b, c)
   @a = a
   @b = b
   @c = c
  end

  def self.sum_of_interior_angles
    180
  end

  def perimeter
    a + b + c
  end
end
```

Seeing how initializer isn't doing anything interesting, we should get by with speccing `.sum_of_interior_angles` and `#perimeter`:

```rb
RSpec.describe Triangle do
  describe ".sum_of_interior_angles" do
    subject(:sum_of_interior_angles) { described_class.sum_of_interior_angles }
    
    it { is_expected.to eq(180) }
  end

  describe "#perimeter" do
    subject(:perimeter) { triangle.perimeter }

    let(:triangle) { described_class.new(3, 4, 5) }
    
    it { is_expected.to eq(12) }
  end  
end
```

## The gripe

My gripe with the spec is minor, but affects nearly all specs, at least for methods without arguments - we've repeated the method name three times, across two lines where one call is plenty.

We could have this:

```rb
RSpec.describe Triangle do
  describe ".sum_of_interior_angles" do    
    it { is_expected.to eq(180) }
  end

  describe "#perimeter" do
    let(:triangle) { described_class.new(3, 4, 5) }
    
    it { is_expected.to eq(12) }
  end  
end
```

How? Easy - convention! Use describe's `.` or `#` to tell whether a class or instance method is being specced and define the subject based on that. For instance method specs infer the name of the instance by underscoring the class name.

But I agree that this may be a bit too implicit and terse for new developers. Here's a bit more explicit version where we specify `instance_name: :triangle` metadata to tell where to read the instance from, and `:implicit_subject` metadata to signal that subject is being defined under the hood.

```rb
RSpec.describe Triangle, instance_name: :triangle do
  describe ".sum_of_interior_angles", :implicit_subject do    
    it { is_expected.to eq(180) }
  end

  describe "#perimeter", :implicit_subject do
    let(:triangle) { described_class.new(3, 4, 5) }
    
    it { is_expected.to eq(12) }
  end  
end
```

Yummy!

## The implementation

To support this behavior a shared context with metadata-based inclusion can be used.

```rb
# in some path that gets loaded for specs, usually spec/support/shared_contexts/*.rb
METHOD_DESCRIPTORS = %w[. #]

RSpec.shared_context "with implicit subject defining", :implicit_subject do |context|
  if metadata[:description].first.in?(METHOD_DESCRIPTORS)   
    method_name = metadata[:description].split("(").first[1..]

    case metadata[:description].first
    when "." # class_method
      subject(method_name) { described_class.send(method_name) }
    when "#" # instance_method
      instance_getter =
        metadata[:instance_name] ||
        :instance # you can do class-name-based inference here if you wish

      subject(method_name) { send(instance_getter).send(method_name) }
    end
  end  
end
```