diff --git a/lib/rspec/graphql_matchers/base_matcher.rb b/lib/rspec/graphql_matchers/base_matcher.rb new file mode 100644 index 0000000..a878dd5 --- /dev/null +++ b/lib/rspec/graphql_matchers/base_matcher.rb @@ -0,0 +1,11 @@ +module RSpec + module GraphqlMatchers + class BaseMatcher + private + + def types_match?(actual_type, expected_type) + expected_type.nil? || expected_type.to_s == actual_type.to_s + end + end + end +end diff --git a/lib/rspec/graphql_matchers/have_a_field.rb b/lib/rspec/graphql_matchers/have_a_field.rb index 17b7839..5832ed4 100644 --- a/lib/rspec/graphql_matchers/have_a_field.rb +++ b/lib/rspec/graphql_matchers/have_a_field.rb @@ -1,20 +1,27 @@ +require_relative 'base_matcher' + module RSpec module GraphqlMatchers - class HaveAField - def initialize(field_name) - @field_name = field_name.to_s - @field_type = @graph_object = nil + class HaveAField < BaseMatcher + def initialize(expected_field_name) + @expected_field_name = expected_field_name.to_s + @expected_field_type = @graph_object = nil end def matches?(graph_object) @graph_object = graph_object - actual_field = @graph_object.fields[@field_name] - actual_field && types_match?(@field_type, actual_field.type) + unless @graph_object.respond_to?(:fields) + raise "Invalid object #{@graph_object} provided to have_a_field " \ + 'matcher. It does not seem to be a valid GraphQL object type.' + end + + @actual_field = @graph_object.fields[@expected_field_name] + valid_field? && types_match?(@actual_field.type, @expected_field_type) end - def that_returns(field_type) - @field_type = field_type + def that_returns(expected_field_type) + @expected_field_type = expected_field_type self end @@ -22,23 +29,38 @@ def that_returns(field_type) alias of_type that_returns def failure_message - "expected #{describe_obj(@graph_object)} to #{description}" + "expected #{describe_obj(@graph_object)} to " \ + "#{description}, #{explanation}." end def description - "define field `#{@field_name}`" + of_type_description + "define field `#{@expected_field_name}`" + of_type_description end private - def of_type_description - return '' unless @field_type + def explanation + return 'but no field was found with that name' unless @actual_field + + "but the field type was `#{@actual_field.type}`" + end + + def valid_field? + unless @expected_field_type.nil? || @actual_field.respond_to?(:type) + error_msg = "The `#{@expected_field_name}` field defined by the GraphQL " \ + 'object does\'t seem valid as it does not respond to #type. ' \ + "\n\n\tThe field found was #{@actual_field.inspect}. " + puts error_msg + raise error_msg + end - " of type `#{@field_type}`" + @actual_field end - def types_match?(expected_type, actual_type) - !expected_type || expected_type.to_s == actual_type.to_s + def of_type_description + return '' unless @expected_field_type + + " of type `#{@expected_field_type}`" end def describe_obj(field) diff --git a/spec/rspec/have_a_field_matcher_spec.rb b/spec/rspec/have_a_field_matcher_spec.rb index ddf437c..ef9dcd3 100644 --- a/spec/rspec/have_a_field_matcher_spec.rb +++ b/spec/rspec/have_a_field_matcher_spec.rb @@ -2,8 +2,17 @@ module RSpec::GraphqlMatchers describe 'expect(a_type).to have_a_field(field_name).that_returns(a_type)' do - subject(:a_type) { double(:type, fields: type_fields) } - let(:type_fields) { { 'id' => double(:field, type: types.String) } } + subject(:a_type) do + types_to_define = type_fields + GraphQL::ObjectType.define do + name 'TestObject' + + types_to_define.each do |fname, ftype| + field fname, ftype + end + end + end + let(:type_fields) { { 'id' => types.String, 'other' => !types.ID } } it { is_expected.to have_a_field(:id) } @@ -17,7 +26,7 @@ module RSpec::GraphqlMatchers it 'fails with a failure message when the type does not define the field' do expect { expect(a_type).to have_a_field(:ids) } - .to fail_with("expected #{a_type.inspect} to define field `ids`") + .to fail_with("expected #{a_type.inspect} to define field `ids`, but no field was found with that name.") end it 'provides a description' do @@ -27,15 +36,53 @@ module RSpec::GraphqlMatchers expect(matcher.description).to eq('define field `id`') end - it 'passes when the type defines the field with correct type' do + it 'passes when the type defines the field with correct type as strings' do expect(a_type).to have_a_field(:id).that_returns('String') + expect(a_type).to have_a_field('other').that_returns('ID!') + end + + it 'passes when the type defines the field with correct type as graphql objects' do + expect(a_type).to have_a_field(:id).that_returns(types.String) + expect(a_type).to have_a_field('other').that_returns(!types.ID) end it 'fails when the type defines a field of the wrong type' do expect { expect(a_type).to have_a_field(:id).returning('String!') } .to fail_with( - "expected #{a_type.inspect} to define field `id` of type `String!`" + "expected #{a_type.inspect} to define field `id` of type `String!`," \ + ' but the field type was `String`.' + ) + + expect { expect(a_type).to have_a_field('other').returning(!types.Int) } + .to fail_with( + "expected #{a_type.inspect} to define field `other` of type `Int!`," \ + ' but the field type was `ID!`.' ) end + + context 'when an invalid type is passed' do + let(:a_type) { double(to_s: 'InvalidObject') } + + it 'fails with a Runtime error' do + expect { expect(a_type).to have_a_field(:id) } + .to raise_error( + RuntimeError, + 'Invalid object InvalidObject provided to have_a_field matcher. ' \ + 'It does not seem to be a valid GraphQL object type.' + ) + end + end + + context 'when a field is found but it does not seem a valid graphql field' do + before do + allow(a_type.fields) + .to receive(:[]).and_return double(inspect: 'AnInvalidField') + end + + it 'fails with a Runtime error' do + expect { expect(a_type).to have_a_field(:id).of_type(!types.Int) } + .to raise_error(RuntimeError) + end + end end end