Logo

dev-resources.site

for different kinds of informations.

DRY up RSpec subject defining

Published at
9/27/2023
Categories
rspec
tests
dry
Author
epigene
Categories
3 categories in total
rspec
open
tests
open
dry
open
Author
7 person written this
epigene
open
DRY up RSpec subject defining

Immediate caveat - usually you want to favour KISS over DRY in specs. I've railed against shared examples (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 (lets 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:

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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

Yummy!

The implementation

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

# 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
Enter fullscreen mode Exit fullscreen mode
tests Article's
30 articles in total
Favicon
Sufficient Software Tests Using Metrics
Favicon
Exploring the Benefits of Integration Testing
Favicon
Best Practices for Effective Automated Integration Tests
Favicon
Automated Tests instrumentation via OpenTelemetry and Aspire Dashboard
Favicon
Integrated tests... are they really important?
Favicon
Focusing on high code coverage can be a trap
Favicon
5 Mistakes to Avoid While Writing Integration Tests
Favicon
Reaching an improved realistic testing approach in the Laravel feature test
Favicon
Desafios Comuns na Escrita de Testes Automatizados: Rumo à Clareza e Padronização - Parte 1
Favicon
Artigo Software Testing: A Research Travelogue - Resumo em PT-BR
Favicon
QA - Définitions et théorie
Favicon
QA - Comment rédiger un test utile ?
Favicon
Applying integration test on NestJS with Jest and GitHub Actions
Favicon
DRY up RSpec subject defining
Favicon
On testing
Favicon
Um vídeo sobre gems e recursos interessantes que podemos integrar com nossas aplicações rails.
Favicon
🩰 Schedule automated tests; become premier ballet artiste
Favicon
Testing Timer-based Logic in Elixir with Klotho Library
Favicon
[Go] How to work with dates in tests
Favicon
Usando o chat do Bing como um aliado para escrever testes de software
Favicon
When and How to Write End-to-End Tests: A Beginner's Guide to Automated E2E Testing
Favicon
Easy Integration Tests for Event-Driven AWS Architectures with EventScout 📨🔭
Favicon
Fix Symfony tests with PHPUnit 10
Favicon
Is programming in TypeScript simply another excuse not to write test in JavaScript?
Favicon
Mocking Interface with jest-mock-extended
Favicon
Do we must implements unit test just to have coverage?
Favicon
Testing a FastAPI application using Ormar models and Alembic migrations
Favicon
How do you deal with test record leaks?
Favicon
Improve your tests with Assert Object Pattern
Favicon
Laravel how to set app environment during tests

Featured ones: