Skip to content

Instantly share code, notes, and snippets.

@justinko
Forked from myronmarston/module_stubbing.rb
Created December 22, 2011 19:52
Show Gist options
  • Save justinko/1511607 to your computer and use it in GitHub Desktop.
Save justinko/1511607 to your computer and use it in GitHub Desktop.
Avdi Grimm's creation, pulled from Objects on Rails
module ModuleStubbing
def stubbed_modules
@stubbed_modules ||= []
end
def stub_module(full_name)
most_shallow_stubbed_module = nil
full_name.to_s.split(/::/).inject(Object) do |context, name|
begin
context.const_get(name)
rescue NameError
most_shallow_stubbed_module ||= [context, name]
context.const_set(name, Module.new)
end
end.tap do
if most_shallow_stubbed_module
stubbed_modules << most_shallow_stubbed_module
end
end
end
def cleanup_stub_modules
stubbed_modules.each do |(context, name)|
context.send(:remove_const, name)
end
end
end
include ModuleStubbing
RSpec.configure do |c|
c.after(:each) { cleanup_stub_modules }
end
@myronmarston
Copy link

  • Hmm...why do you need stub_module available at the top level? I thought it was just meant to be used from RSpec examples and before hooks.
  • I don't get how your change to stub_module still lets it work; the last expression of the method is no longer the inject call, so the result of that won't be returned. I specifically put the logic in a tap block to preserve the module returned by inject as the object to return.
  • You're right, it's good to deal with nils. I didn't think of that case.

@justinko
Copy link
Author

Hmm...why do you need stub_module available at the top level? I thought it was just meant to be used from RSpec examples and before hooks.

If you're spec'ing a model (Foo) that depends on Bar, Bar needs to be available before you can call your top level describe Foo do.

I don't get how your change to stub_module still lets it work; the last expression of the method is no longer the inject call, so the result of that won't be returned. I specifically put the logic in a tap block to preserve the module returned by inject as the object to return.

Why does the module need to be returned? It's using the Object "const" methods, so it will be available. However, doing things like stub_module('Foo').extend(MyModule) would be nice so I'll add tap back in.

@myronmarston
Copy link

Interesting. I hadn't read anything about how this was used. I was just thinking it would be used like this:

describe MyClass
  it 'delegates some logic to another class' do
    foo_bar = stub_module("Foo::Bar")
    foo_bar.should_receive(:bazz).and_return(17)
    MyClass.bam.should eq(some_value_calculated_base_on_17)
  end
end

...but that was just how I imagined it to be used.

@justinko
Copy link
Author

I'm using it at the top level and in before blocks. The top level one is used for the initial describe. The ones in the before blocks are used to "reset" the modules that get wiped in the after hook. I could probably change it to to use the "all" hooks instead.

stub_module('Bar') # Foo depends on Bar
require_relative '../../app/models/foo'

describe Foo do
  before do
    stub_module('Bar') # Since this gets removed in the `after` hook
  end
end

Having to call stub_module('Bar') twice is pretty nasty, but I can't think of another way.

@myronmarston
Copy link

One thought: this is only necessary if you use Bar directly in Foo's class body, right? If you only use Bar in Foo's methods, but not directly in the class body, you wouldn't need to make a stub module before requiring foo. Alternately, full isolation is nice, but if it's important for to use Bar directly in the class body of Foo maybe these things are very coupled and testing it in full isolation may not make sense.

@justinko
Copy link
Author

A simple include Bar in Foo will require you to use stub_module('Bar') in the top level. One way to avoid stub_module is to include it like so:

class Foo
  include Bar if defined?(Bar)
end

But then you're adding test specific logic into the implementation code :(

maybe these things are very coupled and testing it in full isolation may not make sense

Not going to be an option. Currently, this suite takes 11 minutes to run. My goal is to have the capybara specs be the only "full stack" specs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment