Merge pull request #449 from graphql-python/features/test-client-and-snapshot-testing

First version of the Graphene test client and snapshots 💪
This commit is contained in:
Syrus Akbary 2017-04-11 21:54:41 -07:00 committed by GitHub
commit 5f71ac7d41
14 changed files with 553 additions and 314 deletions

View File

@ -10,6 +10,7 @@ Contents:
types/index
execution/index
relay/index
testing/index
Integrations
-----

111
docs/testing/index.rst Normal file
View File

@ -0,0 +1,111 @@
===================
Testing in Graphene
===================
Automated testing is an extremely useful bug-killing tool for the modern developer. You can use a collection of tests a test suite to solve, or avoid, a number of problems:
- When youre writing new code, you can use tests to validate your code works as expected.
- When youre refactoring or modifying old code, you can use tests to ensure your changes havent affected your applications behavior unexpectedly.
Testing a GraphQL application is a complex task, because a GraphQL application is made of several layers of logic schema definition, schema validation, permissions and field resolution.
With Graphene test-execution framework and assorted utilities, you can simulate GraphQL requests, execute mutations, inspect your applications output and generally verify your code is doing what it should be doing.
Testing tools
-------------
Graphene provides a small set of tools that come in handy when writing tests.
Test Client
~~~~~~~~~~~
The test client is a Python class that acts as a dummy GraphQL client, allowing you to test your views and interact with your Graphene-powered application programmatically.
Some of the things you can do with the test client are:
- Simulate Queries and Mutations and observe the response.
- Test that a given query request is rendered by a given Django template, with a template context that contains certain values.
Overview and a quick example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To use the test client, instantiate ``graphene.test.Client`` and retrieve GraphQL responses:
.. code:: python
from graphene.test import Client
def test_hey():
client = Client(my_schema)
executed = client.execute('''{ hey }''')
assert executed == {
'data': {
'hey': 'hello!'
}
}
Execute parameters
~~~~~~~~~~~~~~~~~~
You can also add extra keyword arguments to the ``execute`` method, such as
``context_value``, ``root_value``, ``variable_values``, ...:
.. code:: python
from graphene.test import Client
def test_hey():
client = Client(my_schema)
executed = client.execute('''{ hey }''', context_value={'user': 'Peter'})
assert executed == {
'data': {
'hey': 'hello Peter!'
}
}
Snapshot testing
~~~~~~~~~~~~~~~~
As our APIs evolve, we need to know when our changes introduce any breaking changes that might break
some of the clients of our GraphQL app.
However, writing tests and replicate the same response we expect from our GraphQL application can be
tedious and repetitive task, and sometimes it's easier to skip this process.
Because of that, we recommend the usage of `SnapshotTest <https://github.com/syrusakbary/snapshottest/>`_.
SnapshotTest let us write all this tests in a breeze, as creates automatically the ``snapshots`` for us
the first time the test is executed.
Here is a simple example on how our tests will look if we use ``pytest``:
.. code:: python
def test_hey(snapshot):
client = Client(my_schema)
# This will create a snapshot dir and a snapshot file
# the first time the test is executed, with the response
# of the execution.
snapshot.assert_match(client.execute('''{ hey }'''))
If we are using ``unittest``:
.. code:: python
from snapshottest import TestCase
class APITestCase(TestCase):
def test_api_me(self):
"""Testing the API for /me"""
client = Client(my_schema)
self.assertMatchSnapshot(client.execute('''{ hey }'''))

View File

@ -0,0 +1,202 @@
# -*- coding: utf-8 -*-
# snapshottest: v1 - https://goo.gl/zC4yUc
from __future__ import unicode_literals
from snapshottest import Snapshot
snapshots = Snapshot()
snapshots['test_hero_name_query 1'] = {
'data': {
'hero': {
'name': 'R2-D2'
}
}
}
snapshots['test_hero_name_and_friends_query 1'] = {
'data': {
'hero': {
'id': '2001',
'name': 'R2-D2',
'friends': [
{
'name': 'Luke Skywalker'
},
{
'name': 'Han Solo'
},
{
'name': 'Leia Organa'
}
]
}
}
}
snapshots['test_nested_query 1'] = {
'data': {
'hero': {
'name': 'R2-D2',
'friends': [
{
'name': 'Luke Skywalker',
'appearsIn': [
'NEWHOPE',
'EMPIRE',
'JEDI'
],
'friends': [
{
'name': 'Han Solo'
},
{
'name': 'Leia Organa'
},
{
'name': 'C-3PO'
},
{
'name': 'R2-D2'
}
]
},
{
'name': 'Han Solo',
'appearsIn': [
'NEWHOPE',
'EMPIRE',
'JEDI'
],
'friends': [
{
'name': 'Luke Skywalker'
},
{
'name': 'Leia Organa'
},
{
'name': 'R2-D2'
}
]
},
{
'name': 'Leia Organa',
'appearsIn': [
'NEWHOPE',
'EMPIRE',
'JEDI'
],
'friends': [
{
'name': 'Luke Skywalker'
},
{
'name': 'Han Solo'
},
{
'name': 'C-3PO'
},
{
'name': 'R2-D2'
}
]
}
]
}
}
}
snapshots['test_fetch_luke_query 1'] = {
'data': {
'human': {
'name': 'Luke Skywalker'
}
}
}
snapshots['test_fetch_some_id_query 1'] = {
'data': {
'human': {
'name': 'Luke Skywalker'
}
}
}
snapshots['test_fetch_some_id_query2 1'] = {
'data': {
'human': {
'name': 'Han Solo'
}
}
}
snapshots['test_invalid_id_query 1'] = {
'data': {
'human': None
}
}
snapshots['test_fetch_luke_aliased 1'] = {
'data': {
'luke': {
'name': 'Luke Skywalker'
}
}
}
snapshots['test_fetch_luke_and_leia_aliased 1'] = {
'data': {
'luke': {
'name': 'Luke Skywalker'
},
'leia': {
'name': 'Leia Organa'
}
}
}
snapshots['test_duplicate_fields 1'] = {
'data': {
'luke': {
'name': 'Luke Skywalker',
'homePlanet': 'Tatooine'
},
'leia': {
'name': 'Leia Organa',
'homePlanet': 'Alderaan'
}
}
}
snapshots['test_use_fragment 1'] = {
'data': {
'luke': {
'name': 'Luke Skywalker',
'homePlanet': 'Tatooine'
},
'leia': {
'name': 'Leia Organa',
'homePlanet': 'Alderaan'
}
}
}
snapshots['test_check_type_of_r2 1'] = {
'data': {
'hero': {
'__typename': 'Droid',
'name': 'R2-D2'
}
}
}
snapshots['test_check_type_of_luke 1'] = {
'data': {
'hero': {
'__typename': 'Human',
'name': 'Luke Skywalker'
}
}
}

View File

@ -1,11 +1,12 @@
from graphene.test import Client
from ..data import setup
from ..schema import schema
setup()
client = Client(schema)
def test_hero_name_query():
def test_hero_name_query(snapshot):
query = '''
query HeroNameQuery {
hero {
@ -13,17 +14,11 @@ def test_hero_name_query():
}
}
'''
expected = {
'hero': {
'name': 'R2-D2'
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query))
def test_hero_name_and_friends_query():
def test_hero_name_and_friends_query(snapshot):
query = '''
query HeroNameAndFriendsQuery {
hero {
@ -35,23 +30,10 @@ def test_hero_name_and_friends_query():
}
}
'''
expected = {
'hero': {
'id': '2001',
'name': 'R2-D2',
'friends': [
{'name': 'Luke Skywalker'},
{'name': 'Han Solo'},
{'name': 'Leia Organa'},
]
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query))
def test_nested_query():
def test_nested_query(snapshot):
query = '''
query NestedQuery {
hero {
@ -66,70 +48,10 @@ def test_nested_query():
}
}
'''
expected = {
'hero': {
'name': 'R2-D2',
'friends': [
{
'name': 'Luke Skywalker',
'appearsIn': ['NEWHOPE', 'EMPIRE', 'JEDI'],
'friends': [
{
'name': 'Han Solo',
},
{
'name': 'Leia Organa',
},
{
'name': 'C-3PO',
},
{
'name': 'R2-D2',
},
]
},
{
'name': 'Han Solo',
'appearsIn': ['NEWHOPE', 'EMPIRE', 'JEDI'],
'friends': [
{
'name': 'Luke Skywalker',
},
{
'name': 'Leia Organa',
},
{
'name': 'R2-D2',
},
]
},
{
'name': 'Leia Organa',
'appearsIn': ['NEWHOPE', 'EMPIRE', 'JEDI'],
'friends': [
{
'name': 'Luke Skywalker',
},
{
'name': 'Han Solo',
},
{
'name': 'C-3PO',
},
{
'name': 'R2-D2',
},
]
},
]
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query))
def test_fetch_luke_query():
def test_fetch_luke_query(snapshot):
query = '''
query FetchLukeQuery {
human(id: "1000") {
@ -137,17 +59,10 @@ def test_fetch_luke_query():
}
}
'''
expected = {
'human': {
'name': 'Luke Skywalker',
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query))
def test_fetch_some_id_query():
def test_fetch_some_id_query(snapshot):
query = '''
query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
@ -158,17 +73,10 @@ def test_fetch_some_id_query():
params = {
'someId': '1000',
}
expected = {
'human': {
'name': 'Luke Skywalker',
}
}
result = schema.execute(query, None, variable_values=params)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query, variable_values=params))
def test_fetch_some_id_query2():
def test_fetch_some_id_query2(snapshot):
query = '''
query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
@ -179,17 +87,10 @@ def test_fetch_some_id_query2():
params = {
'someId': '1002',
}
expected = {
'human': {
'name': 'Han Solo',
}
}
result = schema.execute(query, None, variable_values=params)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query, variable_values=params))
def test_invalid_id_query():
def test_invalid_id_query(snapshot):
query = '''
query humanQuery($id: String!) {
human(id: $id) {
@ -200,15 +101,10 @@ def test_invalid_id_query():
params = {
'id': 'not a valid id',
}
expected = {
'human': None
}
result = schema.execute(query, None, variable_values=params)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query, variable_values=params))
def test_fetch_luke_aliased():
def test_fetch_luke_aliased(snapshot):
query = '''
query FetchLukeAliased {
luke: human(id: "1000") {
@ -216,17 +112,10 @@ def test_fetch_luke_aliased():
}
}
'''
expected = {
'luke': {
'name': 'Luke Skywalker',
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query))
def test_fetch_luke_and_leia_aliased():
def test_fetch_luke_and_leia_aliased(snapshot):
query = '''
query FetchLukeAndLeiaAliased {
luke: human(id: "1000") {
@ -237,20 +126,10 @@ def test_fetch_luke_and_leia_aliased():
}
}
'''
expected = {
'luke': {
'name': 'Luke Skywalker',
},
'leia': {
'name': 'Leia Organa',
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query))
def test_duplicate_fields():
def test_duplicate_fields(snapshot):
query = '''
query DuplicateFields {
luke: human(id: "1000") {
@ -263,22 +142,10 @@ def test_duplicate_fields():
}
}
'''
expected = {
'luke': {
'name': 'Luke Skywalker',
'homePlanet': 'Tatooine',
},
'leia': {
'name': 'Leia Organa',
'homePlanet': 'Alderaan',
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query))
def test_use_fragment():
def test_use_fragment(snapshot):
query = '''
query UseFragment {
luke: human(id: "1000") {
@ -293,22 +160,10 @@ def test_use_fragment():
homePlanet
}
'''
expected = {
'luke': {
'name': 'Luke Skywalker',
'homePlanet': 'Tatooine',
},
'leia': {
'name': 'Leia Organa',
'homePlanet': 'Alderaan',
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query))
def test_check_type_of_r2():
def test_check_type_of_r2(snapshot):
query = '''
query CheckTypeOfR2 {
hero {
@ -317,18 +172,10 @@ def test_check_type_of_r2():
}
}
'''
expected = {
'hero': {
'__typename': 'Droid',
'name': 'R2-D2',
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query))
def test_check_type_of_luke():
def test_check_type_of_luke(snapshot):
query = '''
query CheckTypeOfLuke {
hero(episode: EMPIRE) {
@ -337,12 +184,4 @@ def test_check_type_of_luke():
}
}
'''
expected = {
'hero': {
'__typename': 'Human',
'name': 'Luke Skywalker',
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query))

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# snapshottest: v1 - https://goo.gl/zC4yUc
from __future__ import unicode_literals
from snapshottest import Snapshot
snapshots = Snapshot()
snapshots['test_correct_fetch_first_ship_rebels 1'] = {
'data': {
'rebels': {
'name': 'Alliance to Restore the Republic',
'ships': {
'pageInfo': {
'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=',
'endCursor': 'YXJyYXljb25uZWN0aW9uOjA=',
'hasNextPage': True,
'hasPreviousPage': False
},
'edges': [
{
'cursor': 'YXJyYXljb25uZWN0aW9uOjA=',
'node': {
'name': 'X-Wing'
}
}
]
}
}
}
}

View File

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# snapshottest: v1 - https://goo.gl/zC4yUc
from __future__ import unicode_literals
from snapshottest import Snapshot
snapshots = Snapshot()
snapshots['test_mutations 1'] = {
'data': {
'introduceShip': {
'ship': {
'id': 'U2hpcDo5',
'name': 'Peter'
},
'faction': {
'name': 'Alliance to Restore the Republic',
'ships': {
'edges': [
{
'node': {
'id': 'U2hpcDox',
'name': 'X-Wing'
}
},
{
'node': {
'id': 'U2hpcDoy',
'name': 'Y-Wing'
}
},
{
'node': {
'id': 'U2hpcDoz',
'name': 'A-Wing'
}
},
{
'node': {
'id': 'U2hpcDo0',
'name': 'Millenium Falcon'
}
},
{
'node': {
'id': 'U2hpcDo1',
'name': 'Home One'
}
},
{
'node': {
'id': 'U2hpcDo5',
'name': 'Peter'
}
}
]
}
}
}
}
}

View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# snapshottest: v1 - https://goo.gl/zC4yUc
from __future__ import unicode_literals
from snapshottest import Snapshot
snapshots = Snapshot()
snapshots['test_correctly_fetches_id_name_rebels 1'] = {
'data': {
'rebels': {
'id': 'RmFjdGlvbjox',
'name': 'Alliance to Restore the Republic'
}
}
}
snapshots['test_correctly_refetches_rebels 1'] = {
'data': {
'node': {
'id': 'RmFjdGlvbjox',
'name': 'Alliance to Restore the Republic'
}
}
}
snapshots['test_correctly_fetches_id_name_empire 1'] = {
'data': {
'empire': {
'id': 'RmFjdGlvbjoy',
'name': 'Galactic Empire'
}
}
}
snapshots['test_correctly_refetches_empire 1'] = {
'data': {
'node': {
'id': 'RmFjdGlvbjoy',
'name': 'Galactic Empire'
}
}
}
snapshots['test_correctly_refetches_xwing 1'] = {
'data': {
'node': {
'id': 'U2hpcDox',
'name': 'X-Wing'
}
}
}

View File

@ -1,10 +1,13 @@
from graphene.test import Client
from ..data import setup
from ..schema import schema
setup()
client = Client(schema)
def test_correct_fetch_first_ship_rebels():
def test_correct_fetch_first_ship_rebels(snapshot):
query = '''
query RebelsShipsQuery {
rebels {
@ -26,27 +29,4 @@ def test_correct_fetch_first_ship_rebels():
}
}
'''
expected = {
'rebels': {
'name': 'Alliance to Restore the Republic',
'ships': {
'pageInfo': {
'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=',
'endCursor': 'YXJyYXljb25uZWN0aW9uOjA=',
'hasNextPage': True,
'hasPreviousPage': False
},
'edges': [
{
'cursor': 'YXJyYXljb25uZWN0aW9uOjA=',
'node': {
'name': 'X-Wing'
}
}
]
}
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query))

View File

@ -1,10 +1,13 @@
from graphene.test import Client
from ..data import setup
from ..schema import schema
setup()
client = Client(schema)
def test_mutations():
def test_mutations(snapshot):
query = '''
mutation MyMutation {
introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) {
@ -26,51 +29,4 @@ def test_mutations():
}
}
'''
expected = {
'introduceShip': {
'ship': {
'id': 'U2hpcDo5',
'name': 'Peter'
},
'faction': {
'name': 'Alliance to Restore the Republic',
'ships': {
'edges': [{
'node': {
'id': 'U2hpcDox',
'name': 'X-Wing'
}
}, {
'node': {
'id': 'U2hpcDoy',
'name': 'Y-Wing'
}
}, {
'node': {
'id': 'U2hpcDoz',
'name': 'A-Wing'
}
}, {
'node': {
'id': 'U2hpcDo0',
'name': 'Millenium Falcon'
}
}, {
'node': {
'id': 'U2hpcDo1',
'name': 'Home One'
}
}, {
'node': {
'id': 'U2hpcDo5',
'name': 'Peter'
}
}]
},
}
}
}
result = schema.execute(query)
# raise result.errors[0].original_error, None, result.errors[0].stack
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query))

View File

@ -1,8 +1,11 @@
from graphene.test import Client
from ..data import setup
from ..schema import schema
setup()
client = Client(schema)
def test_str_schema():
assert str(schema) == '''schema {
@ -66,7 +69,7 @@ type ShipEdge {
'''
def test_correctly_fetches_id_name_rebels():
def test_correctly_fetches_id_name_rebels(snapshot):
query = '''
query RebelsQuery {
rebels {
@ -75,18 +78,10 @@ def test_correctly_fetches_id_name_rebels():
}
}
'''
expected = {
'rebels': {
'id': 'RmFjdGlvbjox',
'name': 'Alliance to Restore the Republic'
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query))
def test_correctly_refetches_rebels():
def test_correctly_refetches_rebels(snapshot):
query = '''
query RebelsRefetchQuery {
node(id: "RmFjdGlvbjox") {
@ -97,18 +92,10 @@ def test_correctly_refetches_rebels():
}
}
'''
expected = {
'node': {
'id': 'RmFjdGlvbjox',
'name': 'Alliance to Restore the Republic'
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query))
def test_correctly_fetches_id_name_empire():
def test_correctly_fetches_id_name_empire(snapshot):
query = '''
query EmpireQuery {
empire {
@ -117,18 +104,10 @@ def test_correctly_fetches_id_name_empire():
}
}
'''
expected = {
'empire': {
'id': 'RmFjdGlvbjoy',
'name': 'Galactic Empire'
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query))
def test_correctly_refetches_empire():
def test_correctly_refetches_empire(snapshot):
query = '''
query EmpireRefetchQuery {
node(id: "RmFjdGlvbjoy") {
@ -139,18 +118,10 @@ def test_correctly_refetches_empire():
}
}
'''
expected = {
'node': {
'id': 'RmFjdGlvbjoy',
'name': 'Galactic Empire'
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query))
def test_correctly_refetches_xwing():
def test_correctly_refetches_xwing(snapshot):
query = '''
query XWingRefetchQuery {
node(id: "U2hpcDox") {
@ -161,12 +132,4 @@ def test_correctly_refetches_xwing():
}
}
'''
expected = {
'node': {
'id': 'U2hpcDox',
'name': 'X-Wing'
}
}
result = schema.execute(query)
assert not result.errors
assert result.data == expected
snapshot.assert_match(client.execute(query))

39
graphene/test/__init__.py Normal file
View File

@ -0,0 +1,39 @@
import six
from graphql.error import format_error as format_graphql_error
from graphql.error import GraphQLError
from graphene.types.schema import Schema
def default_format_error(error):
if isinstance(error, GraphQLError):
return format_graphql_error(error)
return {'message': six.text_type(error)}
def format_execution_result(execution_result, format_error):
if execution_result:
response = {}
if execution_result.errors:
response['errors'] = [format_error(e) for e in execution_result.errors]
if not execution_result.invalid:
response['data'] = execution_result.data
return response
class Client(object):
def __init__(self, schema, format_error=None, **execute_options):
assert isinstance(schema, Schema)
self.schema = schema
self.execute_options = execute_options
self.format_error = format_error or default_format_error
def execute(self, *args, **kwargs):
return format_execution_result(
self.schema.execute(*args, **dict(self.execute_options, **kwargs)),
self.format_error
)

View File

@ -41,6 +41,7 @@ tests_require = [
'pytest>=2.7.2',
'pytest-benchmark',
'pytest-cov',
'snapshottest',
'coveralls',
'six',
'mock',