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.
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 writing RSpec unit tests for a CommandLineInterface class that I've created for my Directory object. The CommandLineInterface class uses this Directory object to print out a list of people in my Directory. Directory has a #sort_by(param) method that returns an array of strings. The order of the strings depends on the param passed to the #sort_by method (e.g., sort_by("gender"). What would be the correct way to mock out this Directory behavior in my CLI specs? Would I use an instance_double? I am not sure how to do this for a method that takes parameters, like sorting by gender.
I'm only using Ruby and RSpec. No Rails, ActiveRecord, etc. being used here.
Snippets from the class and method I want to mock out:
class Directory
def initialize(params)
#
end
def sort_by(param)
case param
when "gender" then #people.sort_by(&:gender)
when "name" then #people.sort_by(&:name)
else raise ArgumentError
end
end
end
It all depends on how your objects are collaborating.
Some information is lacking in your question:
How does CommandLineInterface use Directory? Does it create an instance by itself or does it receive one as an argument?
Are you testing class methods or instance methods? (Prefer instance methods)
Here's how you could do it if you pass in the dependent object:
require 'rspec/autorun'
class A
def initialize(b)
#b = b
end
def foo(thing)
#b.bar(thing)
end
end
RSpec.describe A do
describe '#foo' do
context 'when given qux' do
let(:b) { double('an instance of B') }
let(:a) { A.new(b) }
it 'calls b.bar with qux' do
expect(b).to receive(:bar).with('qux')
a.foo('qux')
end
end
end
end
If the class initializes the dependant object and it isn't important to know which instance got the message you can do this:
require 'rspec/autorun'
B = Class.new
class A
def initialize
#b = B.new
end
def foo(thing)
#b.bar(thing)
end
end
RSpec.describe A do
describe '#foo' do
context 'when given qux' do
let(:a) { A.new }
it 'calls b.bar with qux' do
expect_any_instance_of(B).to receive(:bar).with('qux')
a.foo('qux')
end
end
end
end
If you just want to stub out the return value and not test whether the exact message was received, you can use allow:
require 'rspec/autorun'
B = Class.new
class A
def initialize
#b = B.new
end
def foo(thing)
thing + #b.bar(thing)
end
end
RSpec.describe A do
describe '#foo' do
context 'when given qux' do
let(:a) { A.new }
it 'returns qux and b.bar' do
allow_any_instance_of(B).to receive(:bar).with('qux') { 'jabber' }
expect(a.foo('qux')).to eq('quxjabber')
end
end
end
end
Consider this code:
def thing_incrementer
lambda do
self.foo +=1
save!
end
end
def increment_thing
with_lock &thing_incrementer
end
How can I write a test which tests that the thing_incrementer is passed with with_lock as a block? If I just wanted to test that it was passed as a parameter (without the leading &) I would do this:
let(:the_lambda){ lambda{} }
x.stub(:thing_incrementer){ the_lambda }
x.should_receive(:with_lock).with(the_lambda)
x.increment_thing
Passing &thing_incrementer passes a proc which gets bound as a block to with_thing. So, just test for that:
expect(subject).to receive(:with_lock).with(no_args) do |&blk|
expect(blk).to be_a(Proc)
end
If you want to pass a lambda as an argument, then you wouldn't prefix it with & and it would just get passed as a normal argument, but then you'd have to call blk.call (or whatever) rather than just yielding to the block.
To check that you're receiving the lambda you want:
class Foo
def incrementor
-> {}
end
def increment
with_lock &incrementor
end
def with_lock
yield
end
end
describe "Lock" do
subject { Foo.new }
let(:the_lambda) { -> {} }
before do
expect(subject).to receive(:incrementor).and_return(the_lambda)
end
it "should receive the_lambda from the incrementor" do
expect(subject).to receive(:with_lock).with(no_args) do |&blk|
expect(blk).to eq(the_lambda)
end
subject.increment
end
end
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
How can a create the following RSpec matcher?
foo.bars.should incude_at_least_one {|bar| bar.id == 42 }
Let me know if I'm reinventing the wheel, but I'm also curious to know how to create a custom matcher that takes a block. Some of the built in matchers do it, so it's possible. I tried this:
RSpec::Matchers.define :incude_at_least_one do |expected|
match do |actual|
actual.each do |item|
return true if yield(item)
end
false
end
end
I aslo tried passing &block at both leves. I'm missing something simple.
I started with the code from Neil Slater, and got it to work:
class IncludeAtLeastOne
def initialize(&block)
#block = block
end
def matches?(actual)
#actual = actual
#actual.any? {|item| #block.call(item) }
end
def failure_message_for_should
"expected #{#actual.inspect} to include at least one matching item, but it did not"
end
def failure_message_for_should_not
"expected #{#actual.inspect} not to include at least one, but it did"
end
end
def include_at_least_one(&block)
IncludeAtLeastOne.new &block
end
There has been discussion about adding such a matcher to rspec. I am not sure about your block question but you could represent this test in the not as elegant looking:
foo.bars.any?{|bar| bar.id == 42}.should be_true
Probably easier than making a custom matcher and should be readable if your test is something like it "should include at least one foo matching the id"
The RSpec DSL won't do it, but you could do something like this:
class IncludeAtLeastOne
def matches?(target)
#target = target
#target.any? do |item|
yield( item )
end
end
def failure_message_for_should
"expected #{#target.inspect} to include at least one thing"
end
def failure_message_for_should_not
"expected #{#target.inspect} not to include at least one"
end
end
def include_at_least_one
IncludeAtLeastOne.new
end
describe "foos" do
it "should contain something interesting" do
[1,2,3].should include_at_least_one { |x| x == 1 }
end
end