Recommended approach to stubbing and unstubbing containers

I’m looking for the best way to work with container stubs in tests. My current main issue is with unstubbing and how to make it both convenient and reliable. I’m using RSpec. I see 3 possible approaches, but they all have their downsides.

1. Unstub all in after(:each)

This is a simple, where in the rspec config you put:

config.after(:each) do
  Slice1::Container.unstub
  Slice2::Container.unstub
end

It has a clear upside that it works for all tests and I don’t need to remember to unstub manually, similarly to database cleaning. It shares the downside of db cleaning as well - seems to be small. For my not-so-big test suite of 45 tests it causes a slowdown of about 40%.

2. Only use blocks

Instead of caring for unstubbing, perhaps I could just only use block syntax in tests?

it "tests with a stub" do
  fake_repo = double("repo")
  Slice1::Container.stub("repositories.account", fake_repo) do
    response = Slice1::Actions::Verify.call({id: 1})
    expect(response.flash[:success]).to eq(...)
  end
end

The problem here is that stub with a block lacks ensure - if the expectation fails, the container is not unstubbed and leaks to other tests. Perhaps this is something that could be fixed upstream in dry-container?

3. Unstub in after block per test case

Last approach is to just manually unstub whenever I use stubbing, so:


RSpec.describe "something" do
  after do
    Slice1::Container.unstub("repositories.account")
  end

  it "does something with a stub" do
    Slice1::Container.stub("repositories.account", something)
    # ...
  end
end

It works best so far, but is also most verbose. Of course, I could abstract it out to some helper method, which - maybe - could just unstub everything.

But I wonder what are the preferred approaches to work with stubs in the community.

I’ve found any approach other than block-based too fragile. Here’s my stub helper:

def stub(key, &block)
  around :each do |example|
    container.stub(key, instance_exec(&block)) { example.run }
  end
end

container is shared context that points to the current app container.

It would be used at the ExampleGroup level like so:

RSpec.describe Foo do
  let(:bar) { FakeBar.new }
  stub("bar") { bar }
  example { ... }
end

Note that if you can inject it directly that is preferred; this should only be done when there is nesting.

RSpec.describe Foo do
  let(:bar) { FakeBar.new }
  subject(:foo) { described_class.new(bar:) }
  example { ... }
end
2 Likes

I endorse everything @alassek has shared here :slight_smile:

Also: if there’s any other approaches to stubbing that you think would be helpful, please share them! This aspect of dry-container/system hasn’t seen much attention since it was first written, so I’m sure there’s more we could do.