I have next scenario:
module Module
class CommandPattern
def initialize(value)
command = []
#var = value['something']
#abc = value['abc']
#command << value
end
def add(value)
#command << value
end
def get_command
#command
end
end
end
module Module
class Implementator
def initialize(value)
#value = value
end
def method_to_test(argument)
var = "command1"
cmd = CommandPattern.new(var)
var2 = "command2"
cmd.add(var2)
var3 = argument
cmd.add(var3)
commands = var + var2 + var3
commands
end
end
end
So, when I'm testing Module::B.method_I_want_to_test, what would be the best practice to mock "var = A.new(some_stuff)"? Beside refactoring and moving this line into separate method, is there some nice way to do this?
Little bit of background on this question - this style (Module::ClassA and Module::ClassB) - I'm using http://naildrivin5.com/gli/ and reason for this approach is that class A is actually implementing Command Pattern.
So issue I was apparently getting was due to wrong way of trying to write specs.
What I did before was (on the way how #spickermann advised):
RSpec.describe Module::Implementator do
describe "#method_to_test" do
let(:command_argument) { "command" }
let(:cmnd) { double(CommandPattern, :new => command_argument, :add => command_argument)}
subject(:method_to_test) do
Implementator.new("value").method_to_test("dejan")
end
before do
allow(CommandPattern).to receive(:new).with(any_args).and_return(cmnd)
allow(CommandPattern).to receive(:add).with(any_args).and_return(cmnd)
end
it 'does something' do
expect{ method_to_test }.not_to raise_error
end
it 'does something else' do
result = method_to_test
expect(result).to eq("command1command2dejan")
end
end
end
Issue was apparently in testing Module::Implementator, didn't realise I can put module around my RSpec.describe block and solve my first issue:
module Module
RSpec.describe Implementator do
describe "#method_to_test" do
let(:command_argument) { "command" }
let(:cmnd) { double(CommandPattern, :new => command_argument, :add => command_argument)}
subject(:method_to_test) do
Implementator.new("value").method_to_test("dejan")
end
before do
allow(CommandPattern).to receive(:new).with(any_args).and_return(cmnd)
allow(CommandPattern).to receive(:add).with(any_args).and_return(cmnd)
end
it 'does something' do
expect{ method_to_test }.not_to raise_error
end
it 'does something else' do
result = method_to_test
expect(result).to eq("command1command2dejan")
end
end
end
end
Another issue I had was global variable keeping YAML structure, which I missed to see and declare in spec_helper.rb
However, thank's to #spickermann's advices, issue is solved.
I would start with something like this:
describe '#method_I_want_to_test' do
let(:something) { # whatever something needs to be }
let(:a) { double(A, # methods you need from a) }
subject(:method_I_want_to_test) do
B.new(something).method_I_want_to_test
end
before do
allow(A).to receive(:new).with(something).and_return(a)
end
it 'does what I expect' do
expect(method_I_want_to_test).to eq(# what do_semething_else returns)
end
end
The interesting part is the before block that stubs the new method on A. It returns always the double defined in the let(:a) line instead of a real instance of A
Related
I am building a DSL and have this module
module EDAApiBuilder
module Client
attr_accessor :api_client, :endpoint, :url
def api_client(api_name)
#apis ||= {}
raise ArgumentError.new('API name already exists.') if #apis.has_key?(api_name)
#api_client = api_name
#apis[#api_client] = {}
yield(self) if block_given?
end
def fetch_client(api_name)
#apis[api_name]
end
def endpoint(endpoint_name)
raise ArgumentError.new("Endpoint #{endpoint_name} already exists for #{#api_client} API client.") if fetch_client(#api_client).has_key?(endpoint_name)
#endpoint = endpoint_name
#apis[#api_client][#endpoint] = {}
yield(self) if block_given?
end
def url=(endpoint_url)
fetch_client(#api_client)[#endpoint]['url'] = endpoint_url
end
end
end
so that I have tests like
context 'errors' do
it 'raises an ArgumentError when trying to create an already existent API client' do
expect {
obj = MixinTester.new
obj.api_client('google')
obj.api_client('google')
}.to raise_error(ArgumentError,'API name already exists.')
end
it 'raises an ArgumentError when trying to create a repeated endpoint for the same API client' do
expect {
obj = MixinTester.new
obj.api_client('google') do |apic|
apic.endpoint('test1')
apic.endpoint('test1')
end
}.to raise_error(ArgumentError,"Endpoint test1 already exists for google API client.")
end
end
I would rather have #api_clientwritten as an assignment block
def api_client=(api_name)
so that I could write
obj = MixinTester.new
obj.api_client = 'google' do |apic| # <=== Notice the difference here
apic.endpoint('test1')
apic.endpoint('test1')
end
because I think this notation (with assignment) is more meaningful. But then, when I run my tests this way I just get an error saying that the keyworkd_do is unexpected in this case.
It seems to me that the definition of an assignment block is syntactic sugar which won't contemplate blocks.
Is this correct? Does anyone have some information about this?
By the way: MixinTester is just a class for testing, defined in my spec/spec_helper.rb as
class MixinTester
include EDAApiBuilder::Client
end
SyntaxError
It seems to me that the definition of an assignment [method] is syntactic
sugar which won't contemplate blocks.
It seems you're right. It looks like no method with = can accept a block, even with the normal method call and no syntactic sugar :
class MixinTester
def name=(name,&block)
end
def set_name(name, &block)
end
end
obj = MixinTester.new
obj.set_name('test') do |x|
puts x
end
obj.name=('test') do |x| # <- syntax error, unexpected keyword_do, expecting end-of-input
puts x
end
Alternative
Hash parameter
An alternative could be written with a Hash :
class MixinTester
def api(params, &block)
block.call(params)
end
end
obj = MixinTester.new
obj.api client: 'google' do |apic|
puts apic
end
#=> {:client=>"google"}
You could adjust the method name and hash parameters to taste.
Parameter with block
If the block belongs to the method parameter, and not the setter method, the syntax is accepted :
def google(&block)
puts "Instantiate Google API"
block.call("custom apic object")
end
class MixinTester
attr_writer :api_client
end
obj = MixinTester.new
obj.api_client = google do |apic|
puts apic
end
# =>
# Instantiate Google API
# custom apic object
It looks weird, but it's pretty close to what you wanted to achieve.
I'm new to writing custom matchers, and most of the examples cover a very minimal set up. What's the proper way to write a matcher that extends a function from a module that has an argument. Do I need to give the actual block the function argument input? Thanks.
# My Example:
RSpec::Matchers.define :total do |expected|
match do |input, actual|
actual.extend(Statistics).sample(input) == expected
end
end
# Before:
describe Statistics do
it 'should not be empty' do
expect(Statistics.sample(input)).not_to be_empty
end
end
Well it depends on what you want to test. If you merely want to test that the module includes a method, maybe something like this:
module Statistics
def sample
end
end
class Test
end
RSpec::Matchers.define :extend_with do |method_name|
match do |klass|
klass.extend(Statistics).respond_to?(method_name)
end
end
describe Statistics do
subject { Test.new }
it { should extend_with(:sample) }
end
If you want to test the value returned, you can add that as an argument, or chain the matcher:
module Statistics
def sample(input)
41 + input
end
end
class Test
end
RSpec::Matchers.define :extend_with do |method_name, input|
match do |klass|
#klass = klass
#klass.extend(Statistics).respond_to?(method_name)
end
chain :returning_value do |value|
#klass.extend(Statistics).__send__(method_name, input) == value
end
end
describe Statistics do
subject { Test.new }
it { should extend_with(:sample) }
it { should extend_with(:sample, 2).returning_value(43) }
end
The matcher DSL is quite flexible. You don't have to be hung up on naming your arguments 'actual' and 'expected' like in the docs -- write the specs so they tell the story of your code.
I have the following test:
let(:client) { Descat::Client.new }
describe 'poblacio' do
it 'should set format correctly' do
client.poblacio('v1','json','dades')
expect (client.instance_variable_get(:format)).to eq('json')
end
end
And I have the following code that is being tested:
module Descat
class Client
BASE_URL = 'http://api.idescat.cat/'
def initialize(attributes = {})
attributes.each do |attr, value|
self.send("#{attr}=", value)
end
end
def poblacio(version, format, operation, *args)
#format = format
end
end
end
WHen running the tests I keep getting '
Failure/Error: expect (client.instance_variable_get(:format)).to eq('json')
NameError:
But, changing the name doesnt help.
'
To use instance_variable_get you have to provide the name starting with "#". In your case:
client.instance_variable_get('#format')
I can take a block of code, instance_exec it, and get the proper result. I would like to take a method off a different object and call one of it's methods in my scope. When I take a method from a different object, turn it into a proc, and then instance_exec it, I don't get the expected result. Code follows.
class Test1
def ohai(arg)
"magic is #{#magic} and arg is #{arg}"
end
end
class Test2
def initialize
#magic = "MAGICAL!"
end
def scope_checking
#magic
end
def do_it
ohai = Test1.new.method(:ohai)
self.instance_exec("foobar", &ohai)
end
end
describe "Test2 and scopes" do
before do
#t2 = Test2.new
end
it "has MAGICAL! in #magic" do
#t2.scope_checking.should == "MAGICAL!"
end
# This one fails :(
it "works like I expect converting a method to a proc" do
val = #t2.do_it
val.should == "magic is MAGICAL! and arg is foobar"
end
it "should work like I expect" do
val = #t2.instance_exec do
"#{#magic}"
end
val.should == "MAGICAL!"
end
end
It seems that, in Ruby, methods defined using def some_method are bound permanently to the class they're defined in.
So, when you call .to_proc on them they keep the binding of their original implementation, and you cannot rebind them. Well, you can, but only to an object of the same type as the first one. It's possible I could do some fancyness with inheritance, but I don't think so.
The solution becomes instead of using methods, I just put actual Procs into variables and use them then, as they're not bound until execution time.
not sure how good of an idea this is, but this passes your tests:
class Test1
def ohai(arg, binding)
eval('"magic is #{#magic} "', binding).to_s + "and arg is #{arg}"
end
end
class Test2
def initialize
#magic = "MAGICAL!"
end
def scope_checking
#magic
end
def get_binding
return binding()
end
def do_it
self.instance_exec(get_binding) {|binding| Test1.new.ohai("foobar", binding) }
end
end
I've got code that only needs to run on a certain version of ActiveRecord (a workaround for a bug on old AR libraries). This code tests the values of ActiveRecord::VERSION constants to see if it needs to be run.
Is there a way to mock out those constants in rspec so I can test that code path without relying on having the right ActiveRecord gem installed on the test machine?
I ended up writing a helper method to let me override constants while executing a block of code:
def with_constants(constants, &block)
constants.each do |constant, val|
Object.const_set(constant, val)
end
block.call
constants.each do |constant, val|
Object.send(:remove_const, constant)
end
end
After putting this code in your spec_helper.rb file, it can be used as follows:
with_constants :RAILS_ROOT => "bar", :RAILS_ENV => "test" do
code goes here ...
end
Hope this works for you.
With RSpec 2.11, constant stubbing is supported out of the box with stub_const:
describe "stub_const" do
it "changes the constant value for the duration of the example" do
stub_const("Foo::SIZE", 10)
expect(Foo::SIZE).to eq(10)
end
end
See Myron Marston's announcement for more details:
http://myronmars.to/n/dev-blog/2012/06/constant-stubbing-in-rspec-2-11
Drew Olson, I took your idea and made a few modifications to add scoping:
class Object
def self.with_constants(constants, &block)
old_constants = Hash.new
constants.each do |constant, val|
old_constants[constant] = const_get(constant)
silence_stderr{ const_set(constant, val) }
end
block.call
old_constants.each do |constant, val|
silence_stderr{ const_set(constant, val) }
end
end
end
After putting this code at specs/support/with_constants.rb file, it can be used as follows:
MyModel.with_constants :MAX_RESULT => 2, :MIN_RESULT => 1 do
code goes here ...
end
Add rescue block is important for ensure restore constant for another tests in test suite !
class Object
class << self
def with_constants(constants, &block)
old_constants = Hash.new
constants.each do |constant, val|
old_constants[constant] = const_get(constant)
Kernel::silence_warnings { const_set(constant, val) }
end
error = nil
begin
block.call
rescue Exception => e
error = e
end
old_constants.each do |constant, val|
Kernel::silence_warnings { const_set(constant, val) }
end
raise error unless error.nil?
end
end
end
Typically
describe "#fail" do
it "should throw error" do
expect {
MyModel.with_constants(:MAX_RESULT => 1) do
# code with throw error
end
}.to raise_error
end
end