0.0
Low commit activity in last 3 years
A long-lived project that still receives updates
A simple dependency injection library for unit testing
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies
 Project Readme

Interjectable

Interjectable is a really simple Ruby library for dependency injection, designed to make unit testing easier.

Installation

It's a gem!

gem 'interjectable'

Usage

Interjectable has one module (Interjectable) and two main methods for defining dependencies, inject and inject_static. Use them like so!

class MyClass
  include Interjectable

  # defines helper methods on instances that memoize values per instance
  inject(:dependency) { SomeOtherClass.new }
  inject(:other_dependency) { AnotherClass.new }

  # defines helper methods on instances that memoize values statically,
  # shared across all instances
  inject_static(:shared_value) { ENV["SOME_VALUE"] }
end

It also includes introspection method injected_methods(include_super = true) (both instance and class-level) to track what dependency methods have been created.

MyClass.injected_methods
# => [:injected_methods, :shared_value, :shared_value=]
MyClass.new.injected_methods
# => [:injected_methods, :dependency, :dependency=, :other_dependency, :other_dependency=, :shared_value, :shared_value=]

This replaces a pattern we've used before, adding default dependencies in the constructor, or as memoized methods.

# OLD WAY, see above
class MyClass
  attr_accessor :dependency

  def initialize(dependency=SomeOtherClass.new)
    @dependency=dependency
  end

  def other_dependency
    @other_dependency ||= AnotherClass.new
  end
end

Dependency Injection + Unit Testing?

Ok but what the heck is dependency injection and what does it have to do with unit tests?

So in the real world, objects depend on other objects: object A uses object B to parse a file. This is normal. But what about when you want to test object A independently from object B? Object B might depend on objects C, D, E and so on, so the test would become needlessly complex.

For the sake of testing object A, we can stub out object B with something fake. But for normal usage of object A, we want to actually use object B (and all its dependencies). We can accommodate both these use cases with dependency injection!

Let's check it out: we can build a class A that normally references B, but in our test we can safely replace B, and don't even have to require or load B at all!

# a.rb
class A
  include Interjectable

  inject(:b) { B.new }
  inject_static(:c) { C.new }

  def read
    b.parse
  end

  def foo
    c.boom
  end
end

# a_spec.rb
require "a"
require "interjectable/rspec"

describe A do
  describe "#read" do
    before do
      # You can use the block form of #test_inject to inject a fake object that references methods on a.
      a.test_inject(:b) { FakeB.new(foo) }

      # You can use the test_inject RSpec helper if you just want to inject an object that doesn't
      # need to reference anything on a.
      test_inject(described_class, :c, instance_double(C, boom: "goat"))
    end

    it "parses from its b, and foos from its c" do
      expect(subject.read).to eq("result")
      expect(subject.foo).to eq("goat")
    end
  end

  # Both Interjectable.test_inject and the RSpec test_inject helper will setup
  # RSpec after hooks to cleanup any test_inject-ed dependencies after the
  # context they are defined in.
  it "doesn't pollute other tests" do
    expect(subject.read).to eq(B.new.parse)
    expect(subject.foo).to eq(C.new.boom)
  end
end

Great, why this library over any other?

Interjectable aims to provide clear defaults for easy debugging.

The other libraries we found used inject/provide setups. That setup is nice because files don't reference each other. However, this can become a headache to debug to actually figure out what is being used where. In our use cases, we use dependency injection for simplified testing, not for hands-free configuration.