diff --git a/README.md b/README.md index 957fd4d..f853b1c 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ gem 'rspec-graphql_matchers' The matchers currently supported are: - `expect(a_graphql_object).to have_a_field(field_name).that_returns(valid_type)` + - `expect(a_graphql_object).to interface(interface_name, ...)` - `expect(a_mutation_type).to have_a_return_field(field_name).returning(valid_type)` - `expect(a_mutation_type).to have_an_input_field(field_name).of_type(valid_type)` - `expect(a_field).to be_of_type(valid_type)` @@ -33,6 +34,8 @@ PostType = GraphQL::ObjectType.define do name "Post" description "A blog post" + interfaces [GraphQL::Relay::Node.interface] + field :id, !types.ID field :comments, !types[types.String] @@ -127,6 +130,20 @@ describe PostType do end ``` +### 4) Test an object's interface implementations: + +```ruby +describe PostType do + it 'interfaces Node' do + expect(subject).to implement('Node') + end + + # Accepts arguments as an array and type objects directly + it { is_expected.to implement(GraphQL::Relay::Node.interface) } + it { is_expected.not_to implement('OtherInterface') } +end +``` + The spec will only pass if all attributes/types specified in the hash are defined on the field. @@ -151,6 +168,5 @@ contributors are expected to adhere to the ## License -The gem is available as open source under the terms of the +The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). - diff --git a/lib/rspec/graphql_matchers/implement.rb b/lib/rspec/graphql_matchers/implement.rb new file mode 100644 index 0000000..da395b6 --- /dev/null +++ b/lib/rspec/graphql_matchers/implement.rb @@ -0,0 +1,46 @@ +require_relative 'base_matcher' + +module RSpec + module GraphqlMatchers + class Implement < BaseMatcher + def initialize(interface_names) + @expected = interface_names.map(&:to_s) + end + + def matches?(graph_object) + @graph_object = graph_object + @actual = actual + @expected.each do |name| + return false unless @actual.include?(name) + end + end + + def failure_message + message = "expected interfaces: #{@expected.join(', ')}\n" + message += "actual interfaces: #{@actual.join(', ')}" + message + end + + def failure_message_when_negated + message = "unexpected interfaces: #{@expected.join(', ')}\n" + message += "actual interfaces: #{@actual.join(', ')}" + message + end + + def description + "implement #{@expected.join(', ')}" + end + + private + + def actual + if @graph_object.respond_to?(:interfaces) + @graph_object.interfaces.map(&:to_s) + else + raise "Invalid object #{@graph_object} provided to #{matcher_name} " \ + 'matcher. It does not seem to be a valid GraphQL object type.' + end + end + end + end +end diff --git a/lib/rspec/graphql_matchers/matchers.rb b/lib/rspec/graphql_matchers/matchers.rb index aae4a46..6601cd2 100644 --- a/lib/rspec/graphql_matchers/matchers.rb +++ b/lib/rspec/graphql_matchers/matchers.rb @@ -2,6 +2,7 @@ require 'rspec/graphql_matchers/be_of_type' require 'rspec/graphql_matchers/accept_arguments' require 'rspec/graphql_matchers/have_a_field' +require 'rspec/graphql_matchers/implement' module RSpec module Matchers @@ -31,5 +32,9 @@ def have_a_return_field(field_name) RSpec::GraphqlMatchers::HaveAField.new(field_name, :return_fields) end alias have_return_field have_a_return_field + + def implement(*interface_names) + RSpec::GraphqlMatchers::Implement.new(interface_names.flatten) + end end end diff --git a/spec/rspec/implement_matcher_spec.rb b/spec/rspec/implement_matcher_spec.rb new file mode 100644 index 0000000..63c1fc1 --- /dev/null +++ b/spec/rspec/implement_matcher_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +module RSpec::GraphqlMatchers + describe 'expect(a_type).to implement(interface_names)' do + AnInterface = GraphQL::InterfaceType.define do + name 'AnInterface' + end + + AnotherInterface = GraphQL::InterfaceType.define do + name 'AnotherInterface' + end + + subject(:a_type) do + GraphQL::ObjectType.define do + name 'TestObject' + interfaces [ + GraphQL::Relay::Node.interface, + AnInterface + ] + end + end + + it { is_expected.to implement('Node') } + it { is_expected.to implement('AnInterface') } + it { is_expected.to implement('Node', 'AnInterface') } + it { is_expected.to implement(['Node']) } + it { is_expected.to implement(['AnInterface']) } + it { is_expected.to implement(['Node', 'AnInterface']) } + it { is_expected.to implement(GraphQL::Relay::Node.interface, AnInterface) } + it { is_expected.to implement([GraphQL::Relay::Node.interface, AnInterface]) } + + it { is_expected.not_to implement('AnotherInterface') } + it { is_expected.not_to implement(['AnotherInterface']) } + it { is_expected.not_to implement(AnotherInterface) } + it { is_expected.not_to implement([AnotherInterface]) } + + it 'fails with a failure message when the type does include the interfaces' do + expect { expect(a_type).to implement('AnotherInterface') } + .to fail_with( + "expected interfaces: AnotherInterface\n" \ + 'actual interfaces: Node, AnInterface' + ) + end + + it 'provides a description' do + matcher = implement('Node, AnInterface') + matcher.matches?(a_type) + + expect(matcher.description).to eq('implement Node, AnInterface') + 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 + end +end