How am I supposed to validate this custom matcher in RSpec - ruby

I have started studying Rspec recently and a got a challenge to solve: Do an object capable of recognize some keys using Regex.
The object needs to pass in a bunch of Rspec tests.
Here are some examples:
describe '#phone?' do
context 'when receive a valid phone' do
[
'+55861107350',
].each do |key|
context "when key is #{key}" do
let(:key) { key }
it { is_expected.to be_a_phone }
end
end
end
context 'when receive an invalid phone' do
[
'55861107350',
'jake#gmail',
'123e4567-e89b-12d3-a456-426655440000'
].each do |key|
context "when key is #{key}" do
it { is_expected.not_to be_a_phone }
end
end
end
What I'm not getting is how am I supposed to validate the be_a_phone matcher, since in the challenge says I can only modify the class file(which is empty and I need to create)
I have been researching about custom matchers and the way you define a custom matcher is by creating the method in another spec file, and supposedly I'm not allow to do that.
Can I make a Rspec::matchers.define :be_a_phone inside a class? or a class file?
My class(it's very raw):
# frozen_string_literal: true
class Key
attr_reader :key, :type
def initialize(key)
#key = key.to_s
#type = ''
self.define_type
end
def set_key(newKey)
#key = newKey
end
def set_type(newType)
#type = newType
end
def define_type
if self.key.scan(/^\+[1-9][0-9]\d{1,14}$/)[0] == self.key
puts "Your key is a valid Phone"
self.set_type("phone")
return "valid"
return self.set_key("invalid")
end
end

Related

Ruby assignment methods won't receive a block?

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.

RSpec: How to mock an object and methods that take parameters

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

RSpec Matchers for function arguments

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.

RSpec - spec fails when trying to call method on initialized attribute

Here is the code for my specs and class:
describe Game do
before(:each) do
#game = Factory.build(:game)
end
describe '#no_books?' do
it 'should return true if books attribute is empty' do
#game.stub(:books).and_return([])
#game.no_books?.should be_true
end
it 'should return false if books attribute is present' do
#game.no_books?.should be_false
end
end
end
class Game
attr_reader :books
def initialize
#books = parse_books
end
def no_books?
#books.empty?
end
protected
def parse_books
# return books
end
end
I then get a friendly spec failure message:
Game#no_books? should return true if books attribute is empty
Failure/Error: #game.no_books?.should be_true
expected false to be true
It's as if that method is getting called before the attribute books get initialized with a value. Can someone explain to me what's going here?
Your no_books? implementation is using the instance variable directly in its check, bypassing your stub. If you change no_books? to return books.empty?, it will call the stub instead.
If you really, really want to keep using the instance variable, you can set it in #game via instance_variable_set like this:
#game.instance_variable_set("#books", [])

Testing a rails model with a singleton in a validate method

Very simple example:
Model:
require 'inventory'
class CustomerOrder < ActiveRecord::Base
validates_presence_of :name
validate :must_have_at_least_one_item, :items_must_exist
before_save :convert_to_internal_items
attr_accessor :items
after_initialize do
#convert the internal_items string into an array
if internal_items
self.items ||= self.internal_items.split(',').collect { |x| x.to_i }
else
# only clobber it if it hasn't been set yet, like in the constructor
self.items ||= []
end
end
private
def convert_to_internal_items
#TODO: convert the items array into a string
self.internal_items = self.items.join(',')
end
def must_have_at_least_one_item
self.items.size >= 1
end
def items_must_exist
self.items.all? do |item|
Inventory.item_exists?(item)
end
end
end
Inventory is a singleton that should provide access to another service out there.
class Inventory
def self.item_exists?(item_id)
# TODO: pretend real code exists here
# MORE CLARITY: this code should be replaced by the mock, because the actual
# inventory service cannot be reached during testing.
end
end
Right now the service does not exist, and so I need to mock out this method for my tests. I'm having trouble doing this the Right Way(tm). I'd like to have it be configurable somehow, so that I can put in the mock during my tests, but have the normal code run in the real world.
There's probably something I'm not wrapping my head around correctly.
EDIT: to be more clear: I need to mock the Inventory class within the validation method of the model. Eventually that will talk to a service that doesn't exist right now. So for my tests, I need to mock it up as if the service I were talking to really existed. Sorry for the confusion :(
Here's what I'd like to have in the specs:
describe CustomerOrder do
it "should not accept valid inventory items" do
#magical mocking that makes Inventory.item_exists? return what I want
magic.should_receive(:item_exists?).with(1).and_return(false)
magic.should_receive(:item_exists?).with(2).and_return(true)
co = CustomerOrder.new(:name => "foobar", :items => [1,2]
co.should_not be_valid
end
it "should be valid with valid inventory items" do
#magical mocking that makes Inventory.item_exists? return what I want
magic.should_receive(:item_exists?).with(3).and_return(true)
magic.should_receive(:item_exists?).with(4).and_return(true)
co = CustomerOrder.new(:name => "foobar", :items => [3,4]
co.should be_valid
end
end
Using rails 3.0.3, rspec 2 and cucumber. Of course, only the rspec part matters.
require 'spec_helper'
describe CustomerOrder do
it "is invalid without an existing Inventory item" do
item = mock('item')
customer = Customer.new(:name=>"Moe")
customer.stub(:items) { [item] }
Inventory.should_receive(:item_exists?).with(item).and_return(true)
customer.should_not be_valid
end
end
Note: untested.
The way I ended up solving this follows
Inventory class:
require 'singleton'
class Inventory
include Singleton
def self.set_mock(mock)
#mock = mock
end
def self.item_exists?(item_id)
return #mock.item_exists?(item_id) if #mock
# TODO: how should I stub this out for the api
end
end
CustomerOrder model:
require 'inventory'
class CustomerOrder < ActiveRecord::Base
validates_presence_of :name
validate :must_have_at_least_one_item, :items_must_exist
before_save :convert_to_internal_items
attr_accessor :items
after_initialize do
#convert the internal_items string into an array
if internal_items
self.items ||= self.internal_items.split(',').collect { |x| x.to_i }
else
# only clobber it if it hasn't been set yet, like in the constructor
self.items ||= []
end
end
private
def convert_to_internal_items
#TODO: convert the items array into a string
self.internal_items = self.items.join(',')
end
def must_have_at_least_one_item
errors.add(:items, "Must have at least one item") unless self.items.size >= 1
end
def items_must_exist
failed = self.items.find_all do |item|
!Inventory.item_exists?(item)
end
if !failed.empty? then
errors.add(:items, "Items with IDs: [#{failed.join(' ')}] are not valid")
end
end
end
CustomerOrder specs:
require 'spec_helper'
describe CustomerOrder do
fixtures :all
before do
fake = double('fake_inventory')
fake.stub(:item_exists?) do |val|
case val
when 1
true
when 2
true
when 3
false
end
end
Inventory.set_mock(fake)
#GRR, skipping my fixtures right now
#valid_order = CustomerOrder.new(:name => "valid order",
:items => [1,2])
end
it "should require a name and at least one item" do
co = CustomerOrder.new(:name => "valid", :items => [1])
co.should be_valid
end
it "should not be valid without any items" do
#valid_order.items = []
#valid_order.should_not be_valid
end
it "should not be valid without a name" do
#valid_order.name = nil
#valid_order.should_not be_valid
end
it "should expose items instead of internal_items" do
#valid_order.should respond_to(:items)
end
it "should be able to treat items like an array" do
#valid_order.items.size.should == 2
#valid_order.items.should respond_to(:<<)
#valid_order.items.should respond_to(:[])
end
it "should store items internally as a comma separated string" do
co = CustomerOrder.new(:name => "name", :items => [1,2])
co.save!
co.internal_items.should == "1,2"
end
it "should convert items to internal_items for saving" do
co = CustomerOrder.new(:name => "my order",
:items => [1,2])
co.name.should == "my order"
co.save!
co.internal_items.should == "1,2"
end
it "loads items from the database into the items array correctly" do
co = CustomerOrder.new(:name => "woot", :items => [2,1])
co.save.should == true
co2 = CustomerOrder.find_by_name("woot")
co2.items.should == [2,1]
end
it "is not valid with items that don't exist" do
#valid_order.items = [3,2,1]
#valid_order.should_not be_valid
end
it "ensures that items exist to be valid" do
#valid_order.items = [1,2]
#valid_order.should be_valid
end
end
This solution works, although it's probably not the best way to inject a mock into the Inventory Service at runtime. I'll try to do a better job of being more clear in the future.

Resources