Skip to content

Instantly share code, notes, and snippets.

@bessey
Last active March 21, 2022 16:02
Show Gist options
  • Select an option

  • Save bessey/91063148696633ba92204b7bfc750c5e to your computer and use it in GitHub Desktop.

Select an option

Save bessey/91063148696633ba92204b7bfc750c5e to your computer and use it in GitHub Desktop.
Beginnings of a reusable GraphQL Ruby type testing toolkit.
# Assumptions
# 1. Your schema implements the Relay Query.node(id:) field, Schema.id_from_object, and Schema.object_from_id
# 2. The types you wish to test implement the Node interface
# 3. (In order to be compatible with Node) the object you test resolves to a single type
# 4. The object you wish to test has a unique identifier
# TODO
# Maybe we can remove 3. by making use of an override in context?
# Since Schema.resolve_type takes a context object, perhaps if you must test an object that is
# presented via multiple types in the schema, you can force the decision via context. I have not
# needed this yet, so not implemented, but seems straightforward.
#
# Maybe we can avoid the need for all this Relay business by re-opening the schema at test runtime and just
# creating the resolvers we need on Query to run the test. This would remove the need for 1. and 2.
#
# I suspect that all object type field tests will end up being of the format `myAssociation { id __typename }`.
# Perhaps we can use schema introspection to know when a field is an object that implements `id`, and do this
# ourselves. This seems a little too magical to me I think. Like, does this make sense to you?:
# user = User.create(friends: 3.times.map { User.create })
# result = MySchemaTypeTester.new.resolve_field("friends", user)
# expect(result).to eq(["1", "2", "3"]) # << How did we know to compare IDs here?
module GraphQL
module Testing
class TypeTester
# GraphQL::Testing::TypeTester provides a convenience API for testing field resolvers via the
# Relay `Query.node(id:)` field. To use it, extend and specify the schema to be tested:
#
# class MySchemaTypeTester < GraphQL::Testing::TypeTester
# schema MySchema
# end
#
#
# Usage:
#
# @example Scalar types
# user = User.create(email: "test@test.com")
# result = MySchemaTypeTester.new.resolve_field(user, "email")
# expect(result).to eq("test@test.com")
#
# @example Scalar types, with context
# user = User.create(email: "test@test.com")
# result = MySchemaTypeTester.new(context: {user: user}).resolve_field(user, "email")
# expect(result).to eq("test@test.com")
#
# @example Scalar type, with variables:
# user = User.create(email: "test@test.com")
# result = MySchemaTypeTester.new.resolve_field(user, "email(only: DOMAIN)")
# expect(result).to eq("test.com")
#
# @example Object types
# user = User.create(friends: 3.times.map { User.create })
# result = MySchemaTypeTester.new.resolve_field(user, "friends { id }")
# expect(result).to eq(["1", "2", "3"])
#
attr_reader :context
class << self
def schema(schema = nil)
@schema = schema if schema
@schema
end
end
def initialize(context: {}, account: nil)
assert_absract_class_extended!
context[:account] ||= account
@context = context
end
# Executes a query fetching the given fields. Infers the type that the fields belong using
# `GraphQL::Schema#resolve_type(object, context)`.
#
# @param object the object used to infer the type, and passed into the resolver for the `field`.
# @param field_and_subfields [String] the field (with subfields for object types) to resolve
# @param context [Hash] field specific context, merged into shared context from `#initialize`
# @return the result of querying for the given field (and optional subfields)
def resolve_field(object, field_and_subfields, context: {})
account ||= context[:account]
field_context = shared_context.merge(context)
type = resolve_type_from_object(object, context)
field_and_subfields = field_and_subfields.camelize(:lower)
# If the user supplied an object field, extract the top level field name
# E.g. "photos { account { id } }" => "photos"
field = field_and_subfields.match(/\A\w+/)[0]
assert_field_exists!(type, field)
query = node_query(type, field_and_subfields)
variables = node_variables(object, type, context)
result = run_graphql_query(query, variables: variables, context: field_context)
if result.errors
raise_query_errors(result.errors)
else
result.data.node.public_send(field)
end
end
private
def schema
self.class.schema
end
def assert_absract_class_extended!
if self === GraphQL::Testing::TypeTester
raise(NotImplementedError, "GraphQL::Testing::TypeTester is an abstract class, extend and set #schema to use")
end
end
def resolve_type_from_object(object, context)
if type = schema.resolve_type(object, context)
return type
end
raise QueryError.new("Could not find infer Type for #{object}")
end
def assert_field_exists!(type, field)
type.fields.fetch(field)
rescue KeyError => message
raise QueryError.new("Could not find Field #{field}\n\n#{message}")
end
def raise_query_errors(errors)
error = QueryError.new(<<~EOT, errors)
GraphQL Query Error!
If you were expecting an error, change your spec to:
expect { result }.to raise_error(GraphQL::QueryError)
#{errors&.map(&:to_hash)}"
EOT
raise error
end
def node_query(type, field)
<<~GRAPHQL
query($id: ID!) {
# Use the Relay node API to fetch the instance by ID from the root node.
node(id: $id) {
... on #{type} {
#{field}
}
}
}
GRAPHQL
end
def node_variables(object, type, context)
{id: schema.id_from_object(object, type, context).to_s}
end
def shared_context
context
end
def run_graphql_query(query, variables: {}, context: {})
account = context[:account]
context = context.reverse_merge(
current_user: account,
account: account,
ability: AbilityDelegator.new(account)
)
result = DirectorySchema.execute(query, context: context, variables: variables)
result = Hashie::Mash.new(result)
end
end
end
end
@bessey

bessey commented Aug 3, 2021 via email

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment