Skip to content

Commit

Permalink
Latest fixes and release preparation
Browse files Browse the repository at this point in the history
  • Loading branch information
khamusa committed Jun 19, 2019
1 parent ccb6d02 commit 0615a60
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 93 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@

### Deprecations

- `.with_metadata` and `.with_property` matchers for fields will be removed on the next release.
- `.with_metadata` and `.with_property` matchers for fields will be removed on the next release;
- `.accept_arguments` (plural form) will be removed on the next release;
- `.accept_argument` (singular form) receiving a hash with a single or multiple arguments will no longer be supported, use `accept_argument(name).of_type('MyType')` instead.

### New features

- Add support for Class-based type definition api (adds support for graphql-ruby v1.8.x). Please note that `.with_metadata` and `.with_property` support has been kept but will only work on fields defined using the legacy api.
- Implemented `accept_argument(arg_name).of_type('MyType')´ matcher, which returns much clearer error messages when the arguments are not found on the target type.

### Bug fixes

Expand Down
108 changes: 36 additions & 72 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@ 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 have_a_field(field_name).of_type(valid_type)`
- `expect(a_graphql_object).to implement(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)`
- `expect(an_input).to accept_arguments(hash_of_arg_names_and_valid_types)`
- `expect(an_input).to accept_arguments(hash_of_arg_names_and_valid_types)`
- `expect(an_input).to accept_argument(argument_name).of_type(valid_type)`

Where a valid type for the expectation is either:

- A reference to the actual type you expect;
- A String representation of a type: `"String!"`, `"Int!"`, `"[String]!"`
- [Recommended] A String representation of a type: `"String!"`, `"Int!"`, `"[String]!"`
(note the exclamation mark at the end, as required by the [GraphQL specs](http://graphql.org/).

For objects defined with the legacy `#define` api, testing `:property`, `:hash_key` and _metadata_ is also possible by chaining `.with_property`, `.with_hash_key` and `.with_metadata`. For example:
Expand All @@ -35,31 +34,22 @@ For objects defined with the legacy `#define` api, testing `:property`, `:hash_k

## Examples

Given a `GraphQL::ObjectType` defined as
Given a GraphQL object defined as

```ruby

PostType = GraphQL::ObjectType.define do
name "Post"
class PostType < GraphQL::Schema::Object
graphql_name "Post"
description "A blog post"

interfaces [GraphQL::Relay::Node.interface]

field :id, !types.ID,
property: :post_id
implements GraphQL::Relay::Node.interface

field :comments,
!types[types.String],
hash_key: :post_comments
field :id, ID, null: false
field :comments, [String], null: false
field :isPublished, Boolean, null: true

field :isPublished,
admin_only: true

field :subposts, PostType do
type !PostType

argument :filter, types.String
argument :id, types.ID
field :subposts, PostType, null: true do
argument :filter, types.String, required: false
argument :id, types.ID, required: false
end
end
```
Expand All @@ -68,13 +58,11 @@ end

```ruby
describe PostType do
it 'defines a field id of type ID!' do
expect(subject).to have_field(:id).that_returns(!types.ID)
end
subject { described_class }

# Fluent alternatives
it { is_expected.to have_field(:id).of_type("ID!") }
it { is_expected.to have_a_field(:id).returning("ID!") }
it { is_expected.to have_field(:id).of_type(!types.ID) }
it { is_expected.to have_field(:comments).of_type("[String!]!") }
it { is_expected.to have_field(:isPublished).of_type("Boolean") }
end
```

Expand All @@ -92,80 +80,54 @@ describe PostType do
describe 'subposts' do
subject { PostType.fields['subposts'] }

# You can use your type object directly when building expectations
it 'has type PostType' do
expect(subject).to be_of_type(!PostType)
end

# Or as usual, a literal String
it { is_expected.to be_of_type('Post!') }
it { is_expected.to be_of_type('Post') }
end
end
```

Keep in mind that when using strings as type expectation you have to use the
type name (`Post`) and not the constant name (`PostType`).

Using your type objects directly is riskier than the string representation, since
renaming the graphql name of an object is potentially a breaking change that
wouldn't get caught by your test suite.

You can also use the built-in [graphql-ruby](https://github.com/rmosolgo/graphql-ruby) scalar types:
### 3) For objects defined using the legacy `#define` api, you can also use `with_property`, `with_hash_key` and `with_metadata`:

```ruby
# ensure you have the GraphQL type definer available in your tests
types = GraphQL::Define::TypeDefiner.instance
PostTypeWithDefineApi = GraphQL::ObjectType.define do
name "DefinedPost"

describe PostType do
describe 'comments' do
subject { PostType.fields['comments'] }
it { is_expected.to be_of_type(!types[types.String]) }
it { is_expected.to be_of_type('[String]!') }
end
end
```
interfaces [GraphQL::Relay::Node.interface]

### 3) Test a specific field with `with_property`, `with_hash_key` and `with_metadata`
field :id, !types.ID, property: :post_id
field :comments, !types[types.String], hash_key: :post_comments
field :isPublished, admin_only: true
end

```ruby
describe PostType do
it { is_expected.to have_a_field(:id).with_property(:post_id) }
describe PostTypeWithDefineApi do
it { is_expected.to have_a_field(:id).of_type('ID!').with_property(:post_id) }
it { is_expected.to have_a_field(:comments).with_hash_key(:post_comments) }
it { is_expected.to have_a_field(:isPublished).with_metadata(admin_only: true) }
end
```

### 4) Test the arguments accepted by a field with `accept_arguments` matcher:
### 4) Test the arguments accepted by a field with `accept_argument` matcher:

```ruby
describe PostType do
describe 'subposts' do
subject { PostType.fields['subposts'] }

let(:a_whole_bunch_of_args) do
{ filter: 'String', id: types.Int, pippo: 'Float', posts: PostType }
end

it 'accepts a filter and an id argument, of types String and ID' do
expect(subject).to accept_arguments(filter: types.String, id: types.ID)
expect(subject).to accept_argument(:filter).of_type('String')
expect(subject).to accept_argument(:id).of_type('ID')
end

# You can also test if a field does not accept args. Not quite useful :D.
it { is_expected.not_to accept_arguments(a_whole_bunch_of_args) }
it { is_expected.not_to accept_argument(:weirdo) }
end
end
```

The spec will only pass if all attributes/types specified in the hash are
defined on the field.

For better fluency, `accept_arguments` is also available in singular form, as
`accept_argument`.

### 5) Test an object's interface implementations:

```ruby
describe PostType do
subject { described_class }

it 'implements interface Node' do
expect(subject).to implement('Node')
end
Expand All @@ -178,6 +140,8 @@ end

## TODO

- Support GraphQL 1.9.x;
- Check the method used for resolving a field;
- New matchers!

## Contributing
Expand Down
73 changes: 73 additions & 0 deletions lib/rspec/graphql_matchers/accept_argument.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
require_relative 'base_matcher'
require_relative './have_a_field_matchers/of_type'

module RSpec
module GraphqlMatchers
class AcceptArgument < BaseMatcher
def initialize(expected_arg_name)
@expectations = []

if expected_arg_name.is_a?(Hash)
(expected_arg_name, expected_type) = expected_arg_name.to_a.first
of_type(expected_type)

warn 'DEPRECATION WARNING: using accept_arguments with a hash will be '\
'deprecated on the next major release. Use the format ' \
"`accept_argument(:argument_name).of_type('ExpectedType!') instead.`"
end

@expected_arg_name = expected_arg_name.to_s
end

def matches?(graph_object)
@graph_object = graph_object

@actual_argument = field_arguments[@expected_arg_name]
return false if @actual_argument.nil?

@results = @expectations.select do |matcher|
!matcher.matches?(@actual_argument)
end

@results.empty?
end

def of_type(expected_field_type)
@expectations << HaveAFieldMatchers::OfType.new(expected_field_type)
self
end

def failure_message
base_msg = "expected #{member_name(@graph_object)} " \
"to accept argument `#{@expected_arg_name}`" \

return "#{base_msg} #{failure_messages.join(', ')}" if @actual_argument

"#{base_msg} but no argument was found with that name"
end

def description
["accept argument `#{@expected_arg_name}`"].concat(descriptions).join(', ')
end

private

def descriptions
@results.map(&:description)
end

def failure_messages
@results.map(&:failure_message)
end

def field_arguments
if @graph_object.respond_to?(:arguments)
@graph_object.public_send(:arguments)
else
raise "Invalid object #{@graph_object} provided to accept_argument " \
'matcher. It does not seem to be a valid GraphQL object type.'
end
end
end
end
end
8 changes: 0 additions & 8 deletions lib/rspec/graphql_matchers/have_a_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,10 @@
module RSpec
module GraphqlMatchers
class HaveAField < BaseMatcher
DESCRIPTIONS = {
type: 'of type `%s`',
property: 'reading from the `%s` property',
hash_key: 'reading from the `%s` hash_key',
metadata: 'with metadata `%s`'
}.freeze

def initialize(expected_field_name, fields = :fields)
@expected_field_name = expected_field_name.to_s
@fields = fields.to_sym
@expectations = []
@descriptions = []
end

def matches?(graph_object)
Expand Down
6 changes: 5 additions & 1 deletion lib/rspec/graphql_matchers/matchers.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'rspec/matchers'
require 'rspec/graphql_matchers/be_of_type'
require 'rspec/graphql_matchers/accept_arguments'
require 'rspec/graphql_matchers/accept_argument'
require 'rspec/graphql_matchers/have_a_field'
require 'rspec/graphql_matchers/implement'

Expand All @@ -10,10 +11,13 @@ def be_of_type(expected)
RSpec::GraphqlMatchers::BeOfType.new(expected)
end

def accept_argument(expected_argument)
RSpec::GraphqlMatchers::AcceptArgument.new(expected_argument)
end

def accept_arguments(expected_args)
RSpec::GraphqlMatchers::AcceptArguments.new(expected_args)
end
alias accept_argument accept_arguments

# rubocop:disable Style/PredicateName
def have_a_field(field_name)
Expand Down
2 changes: 1 addition & 1 deletion lib/rspec/graphql_matchers/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Rspec
module GraphqlMatchers
VERSION = '1.0.0-0.2'.freeze
VERSION = '1.0'.freeze
end
end
Binary file added rspec-graphql_matchers-1.0.0.pre.0.1.gem
Binary file not shown.
Loading

0 comments on commit 0615a60

Please sign in to comment.