Custom assert message for multiple expect statements within match - ruby

I have written a custom match method in Rspec that matches an object against a hash. What I am trying to do is set custom failure messages for each line of the expect.
describe "/cars" do
car = FactoryGirl.create(:car, name: 'Alpha')
describe car do
it "displays single items" do
get cars_path
parsed_response = JSON.parse(response.body)
record_hash = parsed_response['cars'][0]
is_expected.to be_a_car_match_of(record_hash)
end
end
end
RSpec::Matchers.define :be_a_car_match_of do |hash|
match do |car|
expect(car.id).to eq(hash['id'])
expect(car.name).to eq(hash['name'])
end
failure_message do |car|
"expected that #{car} would be a match of #{hash}"
end
end
So what I would like is to have something like the following:
RSpec::Matchers.define :be_a_car_match_of do |hash|
match do |car|
expect(car.id).to eq(hash['id']) 'ids did not match'
expect(car.name).to eq(hash['name']) 'names did not match'
end
end
This would print out a much clearer error message.
I was initially doing this in mini-test but for a variety of reasons (outside of my control) needed to change it to rspec. The code I had in mini-test was:
def assert_car(car, hash)
assert_equal car.id, hash['id'], "ids did not match"
assert_equal car.name, hash['name'], "names did not match"
end
This is what I am trying to replicate.
Here is another example that requires less setup:
require 'rspec/expectations'
RSpec::Matchers.define :be_testing do |expected|
match do |actual|
expect(5).to eq(5)
expect(4).to eq(5)
end
failure_message do
"FAIL"
end
end
describe 'something' do
it 'something else' do
expect("expected").to be_testing('actual')
end
end
When this example is run, "FAIL" is printed out. On the other hand if I had:
describe 'something' do
it 'something else' do
expect(4).to eq(5)
end
end
I would get the following error message:
expected: 5
got: 4
This is what I want. I want to know what part of the custom matcher failed.

You could call the low-level matches? method instead of expect. Something like this:
require 'rspec/expectations'
RSpec::Matchers.define :be_a_positive_integer do
m1, m2 = nil, nil # matchers
r1, r2 = false, false # results
match do |actual|
m1 = be_a Integer # this returns matcher instances
m2 = be > 0
r1 = m1.matches?(actual) # evaluate matchers
r2 = m2.matches?(actual)
r1 && r2 # true if both are true
end
failure_message do |actual| # collect error messages from matchers
messages = []
messages << m1.failure_message unless r1
messages << m2.failure_message unless r2
messages.join("\n")
end
end
describe -1 do
it { is_expected.to be_a_positive_integer }
end
describe 1.0 do
it { is_expected.to be_a_positive_integer }
end
describe -1.0 do
it { is_expected.to be_a_positive_integer }
end
Output:
Failures:
1) -1 should be a positive integer
Failure/Error: it { is_expected.to be_a_positive_integer }
expected: > 0
got: -1
# ./ruby_spec.rb:24:in `block (2 levels) in <top (required)>'
2) 1.0 should be a positive integer
Failure/Error: it { is_expected.to be_a_positive_integer }
expected 1.0 to be a kind of Integer
# ./ruby_spec.rb:28:in `block (2 levels) in <top (required)>'
3) -1.0 should be a positive integer
Failure/Error: it { is_expected.to be_a_positive_integer }
expected -1.0 to be a kind of Integer
expected: > 0
got: -1.0
# ./ruby_spec.rb:32:in `block (2 levels) in <top (required)

I think aggregate_failures is what you are looking for :
It wraps a set of expectations with a block. Within the block, expectation failures will not immediately abort like normal; instead, the failures will be aggregated into a single exception that is raised at the end of the block, allowing you to see all expectations that failed.
See : https://relishapp.com/rspec/rspec-expectations/docs/aggregating-failures

Related

Why does changing the order of 'it' and 'subject' in RSpec change my test result?

The class being tested qa.rb contains the code:
class QA
def initialize(bugs: 0)
#bugs = bugs
end
def speak
"Hello!"
end
def happy?
#bugs > 0
end
def debug
#bugs = 0
end
end
The RSpec file qa_spec.rb contains the code:
require 'rspec'
require_relative 'qa'
RSpec.describe QA do
describe '#happy?' do
context 'when bugs are more than 0' do
it 'returns true' do
subject { described_class.new(bugs: 1) }
expect(subject).to be_happy
end
end
end
end
The test fails when I run it, and gives me this error:
PS C:\Users\Jobla\repos\TDD> rspec qa_spec.rb
F
Failures:
1) QA#happy? when bugs are more than 0 returns true
Failure/Error: expect(subject).to be_happy
expected `#<QA:0x2e0d640 #bugs=0>.happy?` to return true, got false
# ./qa_spec.rb:9:in `block (4 levels) in <top (required)>'
Finished in 0.02999 seconds (files took 0.16995 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./qa_spec.rb:7 # QA#happy? when bugs are more than 0 returns true
However, when I edit qa_spec.rb and I swap the it and subject lines, the test suddenly passes:
require 'rspec'
require_relative 'qa'
RSpec.describe QA do
describe '#happy?' do
context 'when bugs are more than 0' do
subject { described_class.new(bugs: 1) } #swapped with line below
it 'returns true' do #swapped with line above
expect(subject).to be_happy
end
end
end
end
Tests pass:
PS C:\Users\Jobla\repos\TDD> rspec qa_spec.rb
.
Finished in 0.01003 seconds (files took 0.17993 seconds to load)
1 example, 0 failures
Please could someone explain why does swapping the it and subject lines change the result of the test?
subject is designed to be set in context or describe block, but not in it.
If you do not set subject before it then subject would be set automatically by calling new without parameters on described_class. bugs will be set to default 0. After that, you call it with a block subject { described_class.new(bugs: 1) } inside it, it's the same as if you call described_class.new { described_class.new(bugs: 1) } because subject inside it is an instance of QA class.

Rspec not finding class methods

I'm writing some tests for my backend jobs and I'm having a weird issue with rspec not finding my methods.
I wrote a simple class & test to illustrate the issue :
app/interactors/tmp_test.rb :
class TmpTest
def call
a = 10
b = 5
b.substract_two
return a + b
end
def substract_two
c = self - 2
return c
end
end
spec/interactors/tmp_test.rb :
require 'rails_helper'
describe TmpTest do
context 'when doing the substraction' do
it 'return the correct number' do
expect(described_class.call).to eq(13)
end
end
end
output:
TmpTest
when doing the substraction
return the correct number (FAILED - 1)
Failures:
1) TmpTest when doing the substraction return the correct number
Failure/Error: expect(described_class.call).to eq(13)
NoMethodError:
undefined method `call' for TmpTest:Class
# ./spec/interactors/tmp_test.rb:6:in `block (3 levels) in <top (required)>'
Finished in 0.00177 seconds (files took 1.93 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/interactors/tmp_test.rb:5 # TmpTest when doing the substraction return the correct number
It's not a class method, it's an instance method. Your test should look like this:
describe TmpTest do
subject(:instance) { described_class.new }
context 'when doing the subtraction' do
it 'returns the correct number' do
expect(instance.call).to eq(13)
end
end
end
This is a complete mess. Corrected version with comments:
class TmpTest
def call
a = 10
b = 5
# b.substract_two # why do you call method of this class on b?!
a + subtract_two(b)
end
def substract_two(from)
from - 2
end
end
Also: don’t use return in the very last line of the method.

How to check block is called using rspec

I want to check whether the block is called in my function using rspec. Below is my code:
class SP
def speak(options={},&block)
puts "speak called"
block.call()
rescue ZeroDivisionError => e
end
end
describe SP do
it "testing speak functionality can receive a block" do
sp = SP.new
def test_func
a = 1
end
sp_mock = double(sp)
expect(sp_mock).to receive(:speak).with(test_func)
sp.speak(test_func)
end
end
Below is my error:
SP testing speak functionality can receive a block
Failure/Error: block.call()
NoMethodError:
undefined method `call' for nil:NilClass
# ./test.rb:9:in `speak'
# ./test.rb:25:in `block (2 levels) in <top (required)>'
Could you please help. I spent lots of time in that.
You have to use one of RSpec's yield matcher:
describe SP do
it "testing speak functionality can receive a block" do
sp = SP.new
expect { |b| sp.speak(&b) }.to yield_control
end
end
I think Stefan provided the best answer. However I wanted to point out that you should be testing the behaviour of the code instead of implementation details.
describe SP do
it "testing speak functionality can receive a block" do
sp = SP.new
called = false
test_func = -> () { called = true }
sp.speak(&test_func)
expect(called).to eql(true)
end
end

Can rspec's "expect" parse a block to confirm a nested array/grid?

How can the nested array be checked using rspecs expect syntax?
This code block works using rspecs should syntax:
subject.cell_grid.each do |row|
row.is_a?(Array).should be_true
end
...and I think I've got the syntax correct on lines 22 & 23 of the "spec_game_of_life.rb" file, but, when I check the file with rspec I get the following error:
user#ubuntu:~/Ruby/GameOfLife$ rspec spec_game_of_life.rb
..F
Failures:
1) Game of Life world should create proper cell_grid upon initialization
Failure/Error:
expect(subject.cell_grid.each) do |row|
row.is_a?(Array).to be true
end
ArgumentError:
You cannot pass both an argument and a block to `expect`.
# ./spec_game_of_life.rb:22:in `block (3 levels) in <top (required)>'
Finished in 0.0019 seconds (files took 0.11777 seconds to load)
3 examples, 1 failure
Failed examples:
rspec ./spec_game_of_life.rb:19 # Game of Life world should create proper cell_grid upon initialization
I understand that rspec's "should" has been replaced with "expect" per: http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
Initializing a class with a (rows, cols) cell grid - Ruby script, "game_of_life.rb":
1 # basic file
2
3 class World
4 attr_accessor :rows, :cols, :cell_grid
5 def initialize(rows=3, cols=3)
6 #rows = rows
7 #cols = cols
8 #cell_grid = Array.new(rows) do |row|
9 Array.new(cols) do |col|
10 end
11 end
12 end
13 end
Ruby spec file, "spec_game_of_life.rb":
1 # spec file
2
3 require 'rspec'
4 require_relative 'game_of_life.rb'
5
6 describe 'Game of Life' do
7
8 context 'world' do
9 subject { World.new }
10
11 it 'should create a new world object' do
12 expect(subject.is_a?(World)).to be true
13 end
14 it 'should respond to proper methods' do
15 expect(subject.respond_to?(:rows))
16 expect(subject.respond_to?(:cols))
17 expect(subject.respond_to?(:cell_grid))
18 end
19 it 'should create proper cell_grid upon initialization' do
20 expect(subject.cell_grid.is_a?(Array)).to be true
21
22 expect(subject.cell_grid.each) do |row|
23 row.is_a?(Array).to be true
24 end
25 end
26
27 end
28
29 end
FWIW: Ubuntu 14.04, Ruby 2.3.0, rspec 3.5.3 & I'm following along with this "Game of Life" tutorial which uses "should":
https://www.youtube.com/watch?v=Tzs3_pl410M&list=PLMC91Ry9EhRKUn0MIdgXrZiptF7nVyYoQ&index=4
EDIT per answer from Bartek Gladys:
expect{ subject.cell_grid.all? { |k| k.is_a?(Array) } }.to eq true
..F
Failures:
1) Game of Life world should create proper cell_grid upon initialization
Failure/Error: expect{ subject.cell_grid.all? { |k| k.is_a?(Array) } }.to eq true
You must pass an argument rather than a block to use the provided matcher (eq true), or the matcher must implement `supports_block_expectations?`.
# ./spec_game_of_life.rb:22:in `block (3 levels) in <top (required)>'
NOTE:
expect{ subject.cell_grid.all? { |k| k.is_a?(Array) } }.to be true
Failure/Error: expect{ subject.cell_grid.all? { |k| k.is_a?(Array) } }.to be true
You must pass an argument rather than a block to use the provided matcher (equal true), or the matcher must implement `supports_block_expectations?`.
So... how to implement supports_block_expectations??
Researching per: http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/
https://www.relishapp.com/rspec/rspec-expectations/docs/custom-matchers/define-a-matcher-supporting-block-expectations
http://www.relishapp.com/rspec/rspec-expectations/v/3-5/docs
http://rspec.info/
The expect version of your spec is
subject.cell_grid.each do |row|
expect(row.is_a?(Array)).to be_truthy
end
(The be_true matcher no longer exists)
Slightly more naturally you would write
subject.cell_grid.each do |row|
expect(row).to be_an(Array)
end
You could use all? to have only one call to expect but this will lead to less helpful failure messages.
You wouldn't usually pass a block to expect just to make an assertion about a value - typically this is used to check for side effects (such as raising an exception).
try this:
expect{ subject.cell_grid.all? { |k| k.is_a?(Array) } }.to eq true
Not exactly the answer, but as I struggled to find how to define the supports_block_expectations thing, it's below:
Say, we are to give block support to matcher be (can be anything like eq, or equal or even your own matcher thing)
CAUTION: Using existing matchers kinda redefine it. Better name it with something new to avoid trouble.
Definition:
Short hand definition:
RSpec::Matchers.define :be do
match do |actual|
actual.is_a? Proc
end
supports_block_expectations
end
Full definition
RSpec::Matchers.define :equal do
match do |actual|
actual.is_a? Proc
end
def supports_block_expectations?
true # or some logic
end
end
Usage:
RSpec.describe "a custom block matcher" do
it { expect { subject.name }.to be('abc') }
end

New to ruby and trying to fix the error from rspec

Hi I need to know how to do the following
rspec code:
2) WebServer::Htaccess#authorized? for valid-user with valid credentials returns true
Failure/Error: expect(htaccess_valid_user.authorized?(encrypted_string)).to be_true
ArgumentError:
wrong number of arguments calling `authorized?` (1 for 0)
# ./spec/lib/config/htaccess_spec.rb:82:in `(root)'
# ./spec/lib/config/htaccess_spec.rb:44:in `stub_htpwd_file'
# ./spec/lib/config/htaccess_spec.rb:41:in `stub_htpwd_file'
# ./spec/lib/config/htaccess_spec.rb:40:in `stub_htpwd_file'
# ./spec/lib/config/htaccess_spec.rb:81:in `(root)'
Here is the spec.rb file
let(:htaccess_valid_user) { WebServer::Htaccess.new(valid_user_content) }
let(:htaccess_user) { WebServer::Htaccess.new(user_content) }
describe '#authorized?' do
context 'for valid-user' do
context 'with valid credentials' do
it 'returns true' do
stub_htpwd_file do
expect(htaccess_valid_user.authorized?(encrypted_string)).to be_true
end
end
end
context 'with invalid credentials' do
it 'returns false' do
stub_htpwd_file do
expect(htaccess_valid_user.authorized?(encrypted_string('bad user'))).not_to be_nil
expect(htaccess_valid_user.authorized?(encrypted_string('bad user'))).to be_false
end
end
end
end
I am new to ruby TDD, and all I have in my file right now is
def authorized?
end
I am fluent in Node.js but this is completely new to me.
Please help.
It's right there in the error message.
ArgumentError:
wrong number of arguments calling `authorized?` (1 for 0)
You've passed arguments to the authorized? method.
expect(htaccess_valid_user.authorized?(encrypted_string)).to be_true
^^^^^^^^^^^^^^^^^^
But authorized? takes no arguments.
def authorized?
end
Unlike Javascript, Ruby will check you passed in the right number of arguments. If you specify no argument list, the default is to enforce taking no arguments. Add some.
def authorized?(authorization)
end

Resources