Skip to main content
Test Double company logo
Services
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say Hello
Test Double logo
Menu
Services
BackGrid of dots icon
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
Cycle icon
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say hello
Developers
Developers
Developers
Accelerate quality software

Do we need dependency injection in Ruby?

Curious about dependency injection in Ruby? Learn why it can improve your code quality and testing practices with insights from an experienced software consultant.
Kevin Baribeau
|
May 16, 2018
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Let’s start this with a quick example. You’re selling clothes, and your Shirt class looks something like this:

# In real life, these two classes/methods would call an API or something
# Let's ignore those details though :)
class Inventory
  def self.check_availability(product_code); end
end
class Purchaser
  def self.purchase_item(product_code); end
end

class Shirt
  def initialize(product_code)
    @product_code = product_code
  end

  def buy!
    if Inventory.check_availability(product_code)
      Purchaser.purchase_item(product_code)
      true
    else
      false
    end
  end

  private

  attr_reader :product_code
end

… and, you’ve got some tests that looks like this:

require 'rspec'
require_relative 'shirt'

describe Shirt do
  it "doesn't buy shirts when there are none left" do
    shirt = Shirt.new('abc123')
    allow(Inventory).to receive(:check_availability).with('abc123').and_return(false)
    expect(Purchaser).to_not receive(:purchase_item).with('abc123')

    result = shirt.buy!

    expect(result).to eq(false)
  end

  it "buys a shirt when there are shirts available" do
    shirt = Shirt.new('abc123')
    allow(Inventory).to receive(:check_availability).with('abc123').and_return(true)
    expect(Purchaser).to receive(:purchase_item).with('abc123')

    result = shirt.buy!

    expect(result).to eq(true)
  end
end

Let’s say you wanted to avoid hard-coding which classes got called from Shirt. Maybe you might try something like this:

class Inventory
  def self.check_availability(product_code); end
end
class Purchaser
  def self.purchase_item(product_code); end
end

class Shirt
  def initialize(product_code, inventory=Inventory, purchaser=Purchaser)
    @product_code = product_code
    @inventory = inventory
    @purchaser = purchaser
  end

  def buy!
    if inventory.check_availability(product_code)
      purchaser.purchase_item(product_code)
      true
    else
      false
    end
  end

  private

  attr_reader :product_code, :inventory, :purchaser
end

And then, your specs could look like this:

require 'rspec'
require_relative 'shirt_v2'

describe Shirt do
  it "doesn't buy shirts when there are none left" do
    fake_inventory = Object.new
    fake_inventory.define_singleton_method(:check_availability) { |_product_code| false }
    fake_purchaser = spy
    shirt = Shirt.new('small', fake_inventory, fake_purchaser)

    result = shirt.buy!

    expect(result).to eq(false)
    expect(fake_purchaser).not_to have_received(:purchase_item).with('abc123')
  end

  it "buys a shirt when there are shirts available" do
    fake_inventory = Object.new
    fake_inventory.define_singleton_method(:check_availability) do |product_code|
      product_code == 'abc123'
    end
    fake_purchaser = spy
    shirt = Shirt.new('abc123', fake_inventory, fake_purchaser)

    result = shirt.buy!

    expect(fake_purchaser).to have_received(:purchase_item).with('abc123')
    expect(result).to eq(true)
  end
end

If I mention “Dependency Injection," a lot of people seem to think I mean that you should use a framework. In reality, I’m trying to advocate for a design like the second example here.

I’d like to be able to pass in dependencies to an object, instead of having that object rely on class methods or other globally-accessible things.

It’s hard to articulate why I prefer the second example though. I think some people might even be against it – it doesn’t look like typical ruby code. So I have to ask myself, why do I like it?

I tried to come up with some reasons myself. I realized that I’m a big fan of the Arrange, Act, Assert pattern of writing tests. The spec in the first example doesn’t follow that pattern, which bugs me. That’s a pretty small criticism though.

I asked the Test Double #ruby slack channel for some other thoughts.

Dave Mosher pointed out that the allow(x).to receive(y) syntax is a little weird. My thoughts are that it’s common enough though that its intent is probably clear. But if I have a spec that’s failing and I don’t understand why, I’ll probably find myself questioning the weird bits; and the implementation of .to receive is going to be hard to debug. Having a preference for code that’s too-dumb-to-fail over a complex library like rspec-mocks is probably a good thing.

Steven Harman gave me the answer I was really looking for, but couldn’t find myself. The second version of the code has a much clearer dependency structure than the first. Even in the first example, it’s not hard to see that Shirt depends on Inventory and Purchaser, but classes often grow. If this class were a hundred lines long or more, it would be nice to have its dependencies explicitly laid out. In fact, if we explicitly lay out a class’ dependencies, it probably won’t ever grow to a hundred lines or more. Someone will probably (hopefully?) notice that its list of dependencies is getting long and try to split it up.

Lastly, Alex Burkhart pointed me at the section on Dependency Injection in Sandi Metz’s excellent book: Practical Object-Oriented Design in Ruby. Sandi brings up the excellent point that “…knowing the name of a class and the responsibility for knowing the name of a message to send to that class may belong in different objects”. The whole section is great; you should go read it.

I now think I’m convinced that doing Dependency Injection is a Good Thing, even when you have a mocking framework that makes DI technically optional. I like all of these arguments:

  • Objects that support DI tend to have a much clearer set of dependencies
  • Some mocking frameworks force you to avoid the Arrange, Act, Assert pattern in your tests
  • Most mocking frameworks are hard to debug, you don’t want some poor future maintainer to waste time on that; especially if the mocking framework isn’t broken and the problem is actually somewhere else.
  • Sandi Metz has more to say on this topic. We should all go read her books.

Related Insights

🔗
How to debug dependencies with git bisect
🔗
Keep Ruby weird again
🔗
14 tools every Ruby developer will love for performance and debugging

Explore our insights

See all insights
Leadership
Leadership
Leadership
The business of AI: Solve real problems for real people

After participating in the Perplexity AI Business Fellowship, one thing became clear: the AI hype cycle is missing the business fundamentals. Here are 3 evidence-based insights from practitioners actually building or investing in AI solutions that solve real problems.

by
Cathy Colliver
Leadership
Leadership
Leadership
Pragmatic approaches to agentic coding for engineering leaders

Discover essential practices for AI agentic coding to enhance your team’s AI development learning and adoption, while avoiding common pitfalls of vibe coding.

by
A.J. Hekman
by
Aaron Gough
by
Alex Martin
by
Dave Mosher
by
David Lewis
Developers
Developers
Developers
16 things software developers believe, per a Justin Searls survey

Ruby on Rails developer Justin Searls made a personality quiz, and more than 7,000 software developers filled it out. Here's what it revealed.

by
Justin Searls
Letter art spelling out NEAT

Join the conversation

Technology is a means to an end: answers to very human questions. That’s why we created a community for developers and product managers.

Explore the community
Test Double Executive Leadership Team

Learn about our team

Like what we have to say about building great software and great teams?

Get to know us
Test Double company logo
Improving the way the world builds software.
What we do
Services OverviewSoftware DeliveryProduct ManagementLegacy ModernizationDevOpsUpgrade RailsTechnical RecruitmentTechnical Assessments
Who WE ARE
About UsCulture & CareersGreat CausesEDIOur TeamContact UsNews & AwardsN.E.A.T.
Resources
Case StudiesAll InsightsLeadership InsightsDeveloper InsightsProduct InsightsPairing & Office Hours
NEWSLETTER
Sign up hear about our latest innovations.
Your email has been added!
Oops! Something went wrong while submitting the form.
Standard Ruby badge
614.349.4279hello@testdouble.com
Privacy Policy
© 2020 Test Double. All Rights Reserved.