Skip to content
This repository has been archived by the owner on Mar 22, 2023. It is now read-only.

Improve matcher failure message #12

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 58 additions & 17 deletions lib/rspec-virtus/matcher.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
module RSpec
module Virtus
class Matcher
MESSAGES = {
undefined_attribute: 'expected :%{attribute} to be defined in %{subject}',
incorrect_type: 'expected :%{attribute} to be %{expected}, got %{actual}'
}.freeze

attr_accessor :failure_message

def initialize(attribute_name)
@attribute_name = attribute_name
@options = {}
Expand All @@ -18,29 +25,22 @@ def of_type(type, options={})

def matches?(subject)
@subject = subject
attribute_exists? && type_correct?
end

def failure_message
"expected #{@attribute_name} to be defined"
end
failure = validate
@failure_message = compose_message(failure) if failure

def failure_message_when_negated
"expected #{@attribute_name} not to be defined"
failure.nil?
end

private

def attribute
@subject.attribute_set[@attribute_name]
end

def member_type
attribute.member_type.primitive
def validate
return [:undefined_attribute, {}] unless attribute_exists?
return [:incorrect_type, expected_actual_types] unless type_correct?
end

def attribute_type
attribute.primitive
def attribute
@subject.attribute_set[@attribute_name]
end

def attribute_exists?
Expand All @@ -49,13 +49,54 @@ def attribute_exists?

def type_correct?
if @options[:member_type]
member_type == @options[:member_type] && attribute_type == @options[:type]
type_match? && member_type_match?
elsif @options[:type]
attribute_type == @options[:type]
type_match?
else
true
end
end

def type_match?
attribute_match?(attribute, @options[:type])
end

def member_type_match?
attribute_match?(attribute.member_type, @options[:member_type])
end

def attribute_match?(actual, expected)
[actual.class, actual.type, actual.primitive].include?(expected)
end

def compose_message(failure)
key, params = failure
MESSAGES[key] % { attribute: @attribute_name, subject: @subject }.merge(params)
end

def expected_actual_types
{
expected: pretty_type(@options[:type], @options[:member_type]),
actual: pretty_type(
pretty_attribute(attribute),
@options[:member_type] && pretty_attribute(attribute.member_type)
)
}
end

def attribute_type
attribute.primitive
end

def pretty_type(type, member_type = nil)
return type.to_s unless member_type
"#{type}[#{member_type}]"
end

def pretty_attribute(attribute)
return attribute.class if attribute.primitive == BasicObject
attribute.primitive
end
end
end
end
1 change: 1 addition & 0 deletions spec/acceptance/rspec-virtus_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ class DummyPost
describe DummyPost do
it { expect(described_class).to have_attribute(:title) }
it { expect(described_class).to have_attribute(:body).of_type(String) }
it { expect(described_class).to have_attribute(:body).of_type(Axiom::Types::String) }
it { expect(described_class).to have_attribute(:comments).of_type(Array, member_type: String) }
end
79 changes: 67 additions & 12 deletions spec/lib/rspec-virtus/matcher_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
let(:instance) { described_class.new(attribute_name) }
let(:attribute_name) { :the_attribute }

class DummyAttribute < Virtus::Attribute; end
class DummyVirtus
include Virtus.model

attribute :the_attribute, String
attribute :the_array_attribute, Array[String]
attribute :custom_attribute, DummyAttribute
end

describe '#matches?' do
Expand All @@ -20,19 +22,40 @@ class DummyVirtus
it { is_expected.to eql(true) }
end

context 'successful match on attribute name and type' do
context 'successful match on attribute name and primitive type' do
before { instance.of_type(String) }

it { is_expected.to eql(true) }
end

context 'successful match on attribute name and attribute type' do
before { instance.of_type(Axiom::Types::String) }

it { is_expected.to eql(true) }
end

context 'successful match on attribute name and custom type' do
let(:attribute_name) { :custom_attribute }
before { instance.of_type(DummyAttribute) }

it { is_expected.to eql(true) }
end

context 'successful match on attribute name, type and primitive member_type' do
let(:attribute_name) { :the_array_attribute }

before do
instance.of_type(String)
instance.of_type(Array, member_type: String)
end

it { is_expected.to eql(true) }
end

context 'successful match on attribute name, type and member_type' do
context 'successful match on attribute name, type and attribute member_type' do
let(:attribute_name) { :the_array_attribute }

before do
instance.of_type(Array, member_type: String)
instance.of_type(Array, member_type: Axiom::Types::String)
end

it { is_expected.to eql(true) }
Expand Down Expand Up @@ -103,18 +126,50 @@ class DummyVirtus
end

describe '#failure_message' do
subject { instance.failure_message }
subject { instance.tap { |i| i.matches?(DummyVirtus) }.failure_message }

it 'tells you which attribute failed' do
expect(subject).to include(attribute_name.to_s)
context 'on absent attribute' do
let(:attribute_name) { :something_else }

it 'returns absence message' do
message = 'expected :something_else to be defined in DummyVirtus'
expect(subject).to eq message
end
end
end

describe '#failure_message_when_negated' do
subject { instance.failure_message_when_negated }
context 'on incorrect type' do
before { instance.of_type(Integer) }

describe 'attribute type' do
context 'with primitive type' do
it 'returns type primitive' do
message = 'expected :the_attribute to be Integer, got String'
expect(subject).to eq message
end
end

context 'with attribute' do
let(:attribute_name) { :custom_attribute }

it 'returns type class' do
message = 'expected :custom_attribute to be Integer, got DummyAttribute'
expect(subject).to eq message
end
end
end
end

it 'tells you which attribute failed' do
expect(subject).to include(attribute_name.to_s)
context 'on incorrect member_type' do
let(:attribute_name) { :the_array_attribute }

before do
instance.of_type(Array, member_type: Integer)
end

it 'returns wrong type message' do
message = 'expected :the_array_attribute to be Array[Integer], got Array[String]'
expect(subject).to eq message
end
end
end
end