Reputation: 217
I have a module like this that I'm trying to write unit tests for
module MyThing
module Helpers
def self.generate_archive
# ...
::Configuration.export(arg)
rescue ::Configuration::Error => error
raise error
end
end
end
The ::Configuration
module can't exist in my unit testing environment for reasons that are beyond my control, so I need to stub it out. Here's what I've come up with so far.
RSpec.describe 'MyThing' do
it 'generates an archive' do
configuration_stub = stub_const("::Configuration", Module.new)
configuration_error_stub = stub_const("::Configuration::Error", Class.new)
expect_any_instance_of(configuration_stub).to receive(:export).with("arg")
MyThing::Helpers.generate_archive
end
end
This gets me an error.
NoMethodError:
Undefined method `export' for Configuration:Module
If I put the configuration_stub
definition inline with the expect_any_instance_of
like this
RSpec.describe 'MyThing' do
it 'generates an archive' do
configuration_error_stub = stub_const("::Configuration::Error", Class.new)
expect_any_instance_of(stub_const("::Configuration", Module.new)).to receive(:export).with("arg")
MyThing::Helpers.generate_archive
end
end
I also get an error.
NameError:
Uninitialized constant Configuration::Error
...
# --- Caused by: ---
# NoMethodError:
# Undefined method `export' for Configuration:Module
Upvotes: 0
Views: 1179
Reputation: 165446
expect_any_instance_of
works on instance methods. export
is being called as a class method.
Instead, use a normal expect on the class.
expect(Configuration).to receive(:export).with("arg")
Note: it is not necessary to write ::Configuration
in the tests. The ::
is to clarify between MyThing::Helpers::Configuration
and Configuration
.
Note: if you're calling methods directly on Configuration it should probably be a Class not a Module.
Note: instead of calling methods on a class, consider using a Configuration object. App.config.export
where App.config
returns the default Configuration object. This is more flexible.
The problem with that is RSpec will verify that Configuration.export
exists. It doesn't. You could turn off verification, or you could make a real class to test with.
before {
stub_const(
"Configuration",
Class.new do
def self.export(*args)
end
end
)
}
it 'exports' do
expect(Configuration).to receive(:export).with("arg")
Configuration.export("arg")
end
You could write a stub Configuration module for testing only and put it into spec/support, but your tests are increasingly divorced from reality.
The real problem is your project should have a real Configuration class!
I'm going to guess the real Configuration contains production information which cannot be checked into the repository. This is a common anti-pattern. The Configuration module should hide the details of where the configuration values are coming from. There should be independent development, test, and production configurations. There are many ways of doing this, the most common are to use environment specific config files, or to store environment specific config values in environment variables.
While you could mock the Configuration::Error
exception, there should be no reason Configuration::Error
cannot exist in your test environment. Add it and simulate an error like so:
context 'when Configuration.export raises an error'
before {
allow(Configuration).to receive(:export).and_raise(Configuration::Error)
}
it 'does whatever its supposed to do' do
end
end
Upvotes: 1