Skip to content

Commit

Permalink
Graphene Testing (#302)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
TimPansino and umaannamalai committed Jul 29, 2021
1 parent 2c83422 commit 4c5641c
Show file tree
Hide file tree
Showing 10 changed files with 834 additions and 12 deletions.
2 changes: 0 additions & 2 deletions newrelic/hooks/framework_graphql.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from newrelic.common.object_wrapper import function_wrapper, wrap_function_wrapper
from newrelic.core.graphql_utils import graphql_statement
from collections import deque
from copy import copy


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


Expand Down
15 changes: 14 additions & 1 deletion tests/framework_fastapi/_target_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@
# limitations under the License.

from fastapi import FastAPI
from testing_support.asgi_testing import AsgiTest
from graphene import ObjectType, String, Schema
from graphql.execution.executors.asyncio import AsyncioExecutor
from starlette.graphql import GraphQLApp

from newrelic.api.transaction import current_transaction
from testing_support.asgi_testing import AsgiTest

app = FastAPI()

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


class Query(ObjectType):
hello = String()

def resolve_hello(self, info):
return "Hello!"


app.add_route("/graphql", GraphQLApp(executor_class=AsyncioExecutor, schema=Schema(query=Query)))

target_application = AsgiTest(app)
49 changes: 48 additions & 1 deletion tests/framework_fastapi/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
# limitations under the License.

import pytest
from testing_support.fixtures import validate_transaction_metrics
from testing_support.fixtures import dt_enabled, validate_transaction_metrics
from testing_support.validators.validate_span_events import validate_span_events


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

_test()


@dt_enabled
def test_graphql_endpoint(app):
from graphql import __version__ as version

FRAMEWORK_METRICS = [
("Python/Framework/GraphQL/%s" % version, 1),
]
_test_scoped_metrics = [
("GraphQL/resolve/GraphQL/hello", 1),
("GraphQL/operation/GraphQL/query/<anonymous>/hello", 1),
]
_test_unscoped_metrics = [
("GraphQL/all", 1),
("GraphQL/GraphQL/all", 1),
("GraphQL/allWeb", 1),
("GraphQL/GraphQL/allWeb", 1),
] + _test_scoped_metrics

_expected_query_operation_attributes = {
"graphql.operation.type": "query",
"graphql.operation.name": "<anonymous>",
"graphql.operation.query": "{ hello }",
}
_expected_query_resolver_attributes = {
"graphql.field.name": "hello",
"graphql.field.parentType": "Query",
"graphql.field.path": "hello",
"graphql.field.returnType": "String",
}

@validate_span_events(exact_agents=_expected_query_operation_attributes)
@validate_span_events(exact_agents=_expected_query_resolver_attributes)
@validate_transaction_metrics(
"query/<anonymous>/hello",
"GraphQL",
scoped_metrics=_test_scoped_metrics,
rollup_metrics=_test_unscoped_metrics + FRAMEWORK_METRICS,
)
def _test():
response = app.make_request("POST", "/graphql", params="query=%7B%20hello%20%7D")
assert response.status == 200
assert "Hello!" in response.body.decode("utf-8")

_test()
170 changes: 170 additions & 0 deletions tests/framework_graphene/_target_application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from graphene import (
ObjectType,
Field,
String,
Schema,
Mutation as GrapheneMutation,
Int,
List,
NonNull,
Union,
)


class Author(ObjectType):
first_name = String()
last_name = String()


class Book(ObjectType):
id = Int()
name = String()
isbn = String()
author = Field(Author)
branch = String()


class Magazine(ObjectType):
id = Int()
name = String()
issue = Int()
branch = String()


class Item(Union):
class Meta:
types = (Book, Magazine)


class Library(ObjectType):
id = Int()
branch = String()
magazine = Field(List(Magazine))
book = Field(List(Book))


Storage = List(String)



authors = [
Author(
first_name="New",
last_name="Relic",
),
Author(
first_name="Bob",
last_name="Smith",
),
Author(
first_name="Leslie",
last_name="Jones",
),
]

books = [
Book(
id=1,
name="Python Agent: The Book",
isbn="a-fake-isbn",
author=authors[0],
branch="riverside",
),
Book(
id=2,
name="Ollies for O11y: A Sk8er's Guide to Observability",
isbn="a-second-fake-isbn",
author=authors[1],
branch="downtown",
),
Book(
id=3,
name="[Redacted]",
isbn="a-third-fake-isbn",
author=authors[2],
branch="riverside",
),
]

magazines = [
Magazine(id=1, name="Reli Updates Weekly", issue=1, branch="riverside"),
Magazine(id=2, name="Reli Updates Weekly", issue=2, branch="downtown"),
Magazine(id=3, name="Node Weekly", issue=1, branch="riverside"),
]


libraries = ["riverside", "downtown"]
libraries = [
Library(
id=i + 1,
branch=branch,
magazine=[m for m in magazines if m.branch == branch],
book=[b for b in books if b.branch == branch],
)
for i, branch in enumerate(libraries)
]

storage = []



class StorageAdd(GrapheneMutation):
class Arguments:
string = String(required=True)

string = String()

def mutate(parent, info, string):
storage.append(string)
return String(string=string)


class Query(ObjectType):
library = Field(Library, index=Int(required=True))
hello = String()
search = Field(List(Item), contains=String(required=True))
echo = Field(String, echo=String(required=True))
storage = Storage
error = String()

def resolve_library(parent, info, index):
# returns an object that represents a Person
return libraries[index]

def resolve_storage(parent, info):
return storage

def resolve_search(parent, info, contains):
search_books = [b for b in books if contains in b.name]
search_magazines = [m for m in magazines if contains in m.name]
return search_books + search_magazines

def resolve_hello(root, info):
return "Hello!"

def resolve_echo(root, info, echo):
return echo

def resolve_error(root, info):
raise RuntimeError("Runtime Error!")

error_non_null = Field(NonNull(String), resolver=resolve_error)


class Mutation(ObjectType):
storage_add = StorageAdd.Field()

_target_application = Schema(query=Query, mutation=Mutation, auto_camelcase=False)
51 changes: 51 additions & 0 deletions tests/framework_graphene/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest
import six
from testing_support.fixtures import (
code_coverage_fixture,
collector_agent_registration_fixture,
collector_available_fixture,
)

_coverage_source = [
"newrelic.hooks.framework_graphql",
]

code_coverage = code_coverage_fixture(source=_coverage_source)

_default_settings = {
"transaction_tracer.explain_threshold": 0.0,
"transaction_tracer.transaction_threshold": 0.0,
"transaction_tracer.stack_trace_threshold": 0.0,
"debug.log_data_collector_payloads": True,
"debug.record_transaction_failure": True,
}

collector_agent_registration = collector_agent_registration_fixture(
app_name="Python Agent Test (framework_graphql)",
default_settings=_default_settings,
)


@pytest.fixture(scope="session")
def app():
from _target_application import _target_application

return _target_application


if six.PY2:
collect_ignore = ["test_application_async.py"]
Loading

0 comments on commit 4c5641c

Please sign in to comment.