Skip to content

Commit 4c5641c

Browse files
Graphene Testing (#302)
* Test basic working. * Fix Graphql tests misnamed field. * Finalize graphene test porting. * Updates after rebase. * Remove unused import. * Expand tox matrix for graphene. * Fix py2 tests for graphene. * Fix py2 tests for graphene. * Remove unused code. * Final testing update. Co-authored-by: Uma Annamalai <[email protected]> * Minor testing tweaks. * Add testing for graphene schemas in fastapi. Co-authored-by: Uma Annamalai <[email protected]>
1 parent 2c83422 commit 4c5641c

10 files changed

+834
-12
lines changed

newrelic/hooks/framework_graphql.py

-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
from newrelic.common.object_wrapper import function_wrapper, wrap_function_wrapper
2222
from newrelic.core.graphql_utils import graphql_statement
2323
from collections import deque
24-
from copy import copy
2524

2625

2726
GRAPHQL_IGNORED_FIELDS = frozenset(("id", "__typename"))
@@ -426,7 +425,6 @@ def wrap_graphql_impl(wrapped, instance, args, kwargs):
426425
trace.statement = graphql_statement(query)
427426
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
428427
result = wrapped(*args, **kwargs)
429-
# transaction.set_transaction_name(transaction_name, "GraphQL", priority=14)
430428
return result
431429

432430

tests/framework_fastapi/_target_application.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@
1313
# limitations under the License.
1414

1515
from fastapi import FastAPI
16-
from testing_support.asgi_testing import AsgiTest
16+
from graphene import ObjectType, String, Schema
17+
from graphql.execution.executors.asyncio import AsyncioExecutor
18+
from starlette.graphql import GraphQLApp
19+
1720
from newrelic.api.transaction import current_transaction
21+
from testing_support.asgi_testing import AsgiTest
1822

1923
app = FastAPI()
2024

@@ -31,4 +35,13 @@ async def non_sync():
3135
return {}
3236

3337

38+
class Query(ObjectType):
39+
hello = String()
40+
41+
def resolve_hello(self, info):
42+
return "Hello!"
43+
44+
45+
app.add_route("/graphql", GraphQLApp(executor_class=AsyncioExecutor, schema=Schema(query=Query)))
46+
3447
target_application = AsgiTest(app)

tests/framework_fastapi/test_application.py

+48-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
# limitations under the License.
1414

1515
import pytest
16-
from testing_support.fixtures import validate_transaction_metrics
16+
from testing_support.fixtures import dt_enabled, validate_transaction_metrics
17+
from testing_support.validators.validate_span_events import validate_span_events
1718

1819

1920
@pytest.mark.parametrize("endpoint,transaction_name", (
@@ -28,3 +29,49 @@ def _test():
2829
assert response.status == 200
2930

3031
_test()
32+
33+
34+
@dt_enabled
35+
def test_graphql_endpoint(app):
36+
from graphql import __version__ as version
37+
38+
FRAMEWORK_METRICS = [
39+
("Python/Framework/GraphQL/%s" % version, 1),
40+
]
41+
_test_scoped_metrics = [
42+
("GraphQL/resolve/GraphQL/hello", 1),
43+
("GraphQL/operation/GraphQL/query/<anonymous>/hello", 1),
44+
]
45+
_test_unscoped_metrics = [
46+
("GraphQL/all", 1),
47+
("GraphQL/GraphQL/all", 1),
48+
("GraphQL/allWeb", 1),
49+
("GraphQL/GraphQL/allWeb", 1),
50+
] + _test_scoped_metrics
51+
52+
_expected_query_operation_attributes = {
53+
"graphql.operation.type": "query",
54+
"graphql.operation.name": "<anonymous>",
55+
"graphql.operation.query": "{ hello }",
56+
}
57+
_expected_query_resolver_attributes = {
58+
"graphql.field.name": "hello",
59+
"graphql.field.parentType": "Query",
60+
"graphql.field.path": "hello",
61+
"graphql.field.returnType": "String",
62+
}
63+
64+
@validate_span_events(exact_agents=_expected_query_operation_attributes)
65+
@validate_span_events(exact_agents=_expected_query_resolver_attributes)
66+
@validate_transaction_metrics(
67+
"query/<anonymous>/hello",
68+
"GraphQL",
69+
scoped_metrics=_test_scoped_metrics,
70+
rollup_metrics=_test_unscoped_metrics + FRAMEWORK_METRICS,
71+
)
72+
def _test():
73+
response = app.make_request("POST", "/graphql", params="query=%7B%20hello%20%7D")
74+
assert response.status == 200
75+
assert "Hello!" in response.body.decode("utf-8")
76+
77+
_test()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
from graphene import (
15+
ObjectType,
16+
Field,
17+
String,
18+
Schema,
19+
Mutation as GrapheneMutation,
20+
Int,
21+
List,
22+
NonNull,
23+
Union,
24+
)
25+
26+
27+
class Author(ObjectType):
28+
first_name = String()
29+
last_name = String()
30+
31+
32+
class Book(ObjectType):
33+
id = Int()
34+
name = String()
35+
isbn = String()
36+
author = Field(Author)
37+
branch = String()
38+
39+
40+
class Magazine(ObjectType):
41+
id = Int()
42+
name = String()
43+
issue = Int()
44+
branch = String()
45+
46+
47+
class Item(Union):
48+
class Meta:
49+
types = (Book, Magazine)
50+
51+
52+
class Library(ObjectType):
53+
id = Int()
54+
branch = String()
55+
magazine = Field(List(Magazine))
56+
book = Field(List(Book))
57+
58+
59+
Storage = List(String)
60+
61+
62+
63+
authors = [
64+
Author(
65+
first_name="New",
66+
last_name="Relic",
67+
),
68+
Author(
69+
first_name="Bob",
70+
last_name="Smith",
71+
),
72+
Author(
73+
first_name="Leslie",
74+
last_name="Jones",
75+
),
76+
]
77+
78+
books = [
79+
Book(
80+
id=1,
81+
name="Python Agent: The Book",
82+
isbn="a-fake-isbn",
83+
author=authors[0],
84+
branch="riverside",
85+
),
86+
Book(
87+
id=2,
88+
name="Ollies for O11y: A Sk8er's Guide to Observability",
89+
isbn="a-second-fake-isbn",
90+
author=authors[1],
91+
branch="downtown",
92+
),
93+
Book(
94+
id=3,
95+
name="[Redacted]",
96+
isbn="a-third-fake-isbn",
97+
author=authors[2],
98+
branch="riverside",
99+
),
100+
]
101+
102+
magazines = [
103+
Magazine(id=1, name="Reli Updates Weekly", issue=1, branch="riverside"),
104+
Magazine(id=2, name="Reli Updates Weekly", issue=2, branch="downtown"),
105+
Magazine(id=3, name="Node Weekly", issue=1, branch="riverside"),
106+
]
107+
108+
109+
libraries = ["riverside", "downtown"]
110+
libraries = [
111+
Library(
112+
id=i + 1,
113+
branch=branch,
114+
magazine=[m for m in magazines if m.branch == branch],
115+
book=[b for b in books if b.branch == branch],
116+
)
117+
for i, branch in enumerate(libraries)
118+
]
119+
120+
storage = []
121+
122+
123+
124+
class StorageAdd(GrapheneMutation):
125+
class Arguments:
126+
string = String(required=True)
127+
128+
string = String()
129+
130+
def mutate(parent, info, string):
131+
storage.append(string)
132+
return String(string=string)
133+
134+
135+
class Query(ObjectType):
136+
library = Field(Library, index=Int(required=True))
137+
hello = String()
138+
search = Field(List(Item), contains=String(required=True))
139+
echo = Field(String, echo=String(required=True))
140+
storage = Storage
141+
error = String()
142+
143+
def resolve_library(parent, info, index):
144+
# returns an object that represents a Person
145+
return libraries[index]
146+
147+
def resolve_storage(parent, info):
148+
return storage
149+
150+
def resolve_search(parent, info, contains):
151+
search_books = [b for b in books if contains in b.name]
152+
search_magazines = [m for m in magazines if contains in m.name]
153+
return search_books + search_magazines
154+
155+
def resolve_hello(root, info):
156+
return "Hello!"
157+
158+
def resolve_echo(root, info, echo):
159+
return echo
160+
161+
def resolve_error(root, info):
162+
raise RuntimeError("Runtime Error!")
163+
164+
error_non_null = Field(NonNull(String), resolver=resolve_error)
165+
166+
167+
class Mutation(ObjectType):
168+
storage_add = StorageAdd.Field()
169+
170+
_target_application = Schema(query=Query, mutation=Mutation, auto_camelcase=False)

tests/framework_graphene/conftest.py

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import pytest
16+
import six
17+
from testing_support.fixtures import (
18+
code_coverage_fixture,
19+
collector_agent_registration_fixture,
20+
collector_available_fixture,
21+
)
22+
23+
_coverage_source = [
24+
"newrelic.hooks.framework_graphql",
25+
]
26+
27+
code_coverage = code_coverage_fixture(source=_coverage_source)
28+
29+
_default_settings = {
30+
"transaction_tracer.explain_threshold": 0.0,
31+
"transaction_tracer.transaction_threshold": 0.0,
32+
"transaction_tracer.stack_trace_threshold": 0.0,
33+
"debug.log_data_collector_payloads": True,
34+
"debug.record_transaction_failure": True,
35+
}
36+
37+
collector_agent_registration = collector_agent_registration_fixture(
38+
app_name="Python Agent Test (framework_graphql)",
39+
default_settings=_default_settings,
40+
)
41+
42+
43+
@pytest.fixture(scope="session")
44+
def app():
45+
from _target_application import _target_application
46+
47+
return _target_application
48+
49+
50+
if six.PY2:
51+
collect_ignore = ["test_application_async.py"]

0 commit comments

Comments
 (0)