Skip to content

Commit

Permalink
Run integration tests through the GraphQL API (#206)
Browse files Browse the repository at this point in the history
With this change, integration tests will route calls directly through the GraphQL API and not just to the translator. Thus we can find potential errors in the return values faster.
  • Loading branch information
Andy2003 authored Apr 14, 2021
1 parent 268a40d commit a92b8ca
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 45 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/pr-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ jobs:
java-version: 11
distribution: adopt
- name: Run Maven build
# TODO after fixing all integration tests
# run: ./mvnw --no-transfer-progress -Dneo4j-graphql-java.integration-tests=true clean compile test
run: ./mvnw --no-transfer-progress clean compile test
run: ./mvnw --no-transfer-progress -Dneo4j-graphql-java.integration-tests=true -Dneo4j-graphql-java.generate-test-file-diff=false clean compile test
- name: Publish Unit Test Results
uses: EnricoMi/publish-unit-test-result-action@v1
if: always()
Expand Down
3 changes: 3 additions & 0 deletions core/src/test/kotlin/org/neo4j/graphql/CypherTests.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.neo4j.graphql

import apoc.coll.Coll
import apoc.cypher.CypherFunctions
import org.junit.jupiter.api.*
import org.neo4j.graphql.utils.CypherTestSuite
Expand All @@ -22,6 +23,8 @@ class CypherTests {
.newInProcessBuilder(Path.of("target/test-db"))
.withProcedure(apoc.cypher.Cypher::class.java)
.withFunction(CypherFunctions::class.java)
.withProcedure(Coll::class.java)
.withFunction(Coll::class.java)
.build()
}
}
Expand Down
117 changes: 83 additions & 34 deletions core/src/test/kotlin/org/neo4j/graphql/utils/CypherTestSuite.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package org.neo4j.graphql.utils

import graphql.ExecutionInput
import graphql.GraphQL
import graphql.schema.DataFetcher
import graphql.schema.DataFetchingEnvironment
import graphql.schema.GraphQLSchema
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Assumptions
import org.junit.jupiter.api.DynamicNode
Expand Down Expand Up @@ -44,9 +49,8 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest
if (neo4j != null) {
val testData = globalBlocks[TEST_DATA_MARKER]
val response = getOrCreateBlock(codeBlocks, GRAPHQL_RESPONSE_MARKER, "GraphQL-Response")

if (testData != null && response != null) {
tests.add(integrationTest(testData, response, result))
tests.add(integrationTest(title, globalBlocks, codeBlocks, testData, response))
}
}

Expand All @@ -56,17 +60,27 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest
return tests
}

private fun createTransformationTask(title: String, globalBlocks: Map<String, ParsedBlock>, codeBlocks: Map<String, ParsedBlock>): () -> Cypher {
private fun createSchema(
globalBlocks: Map<String, ParsedBlock>,
codeBlocks: Map<String, ParsedBlock>,
dataFetchingInterceptor: DataFetchingInterceptor? = null
): GraphQLSchema {
val schemaString = globalBlocks[SCHEMA_MARKER]?.code()
?: throw IllegalStateException("Schema should be defined")
val schemaConfig = (codeBlocks[SCHEMA_CONFIG_MARKER] ?: globalBlocks[SCHEMA_CONFIG_MARKER])?.code()
?.let { return@let MAPPER.readValue(it, SchemaConfig::class.java) }
?: SchemaConfig()
return SchemaBuilder.buildSchema(schemaString, schemaConfig, dataFetchingInterceptor)
}

private fun createTransformationTask(
title: String,
globalBlocks: Map<String, ParsedBlock>,
codeBlocks: Map<String, ParsedBlock>
): () -> Cypher {
val transformationTask = FutureTask {

val schemaConfig = (codeBlocks[SCHEMA_CONFIG_MARKER] ?: globalBlocks[SCHEMA_CONFIG_MARKER])?.code()
?.let { return@let MAPPER.readValue(it, SchemaConfig::class.java) }
?: SchemaConfig()
val schema = SchemaBuilder.buildSchema(schemaString, schemaConfig)

val schema = createSchema(globalBlocks, codeBlocks)

val request = codeBlocks[GRAPHQL_MARKER]?.code()
?: throw IllegalStateException("missing graphql for $title")
Expand Down Expand Up @@ -139,37 +153,72 @@ class CypherTestSuite(fileName: String, val neo4j: Neo4j? = null) : AsciiDocTest
}
}

private fun integrationTest(testData: ParsedBlock, response: ParsedBlock, result: () -> Cypher): DynamicNode = DynamicTest.dynamicTest("Integration Test", response.uri) {
neo4j?.defaultDatabaseService()?.let { db ->
db.executeTransactionally("MATCH (n) DETACH DELETE n")
if (testData.code().isNotBlank()) {
testData.code()
.split(";")
.filter { it.isNotBlank() }
.forEach { db.executeTransactionally(it) }
}
val (cypher, params, type, variable) = result()
val values = db.executeTransactionally(cypher, params) { result ->
mutableMapOf(variable to result.stream().map { it[variable] }.let {
when {
type?.isList() == true -> it.toList()
else -> it.findFirst().orElse(null)
private fun setupDataFetchingInterceptor(testData: ParsedBlock): DataFetchingInterceptor {
return object : DataFetchingInterceptor {
override fun fetchData(env: DataFetchingEnvironment, delegate: DataFetcher<Cypher>): Any? = neo4j
?.defaultDatabaseService()?.let { db ->
db.executeTransactionally("MATCH (n) DETACH DELETE n")
if (testData.code().isNotBlank()) {
testData.code()
.split(";")
.filter { it.isNotBlank() }
.forEach { db.executeTransactionally(it) }
}
})
}
val (cypher, params, type, variable) = delegate.get(env)
return db.executeTransactionally(cypher, params) { result ->
result.stream().map { it[variable] }.let {
when {
type?.isList() == true -> it.toList()
else -> it.findFirst().orElse(null)
}
}

if (response.code.isEmpty()) {
}
}
}
}

private fun integrationTest(
title: String,
globalBlocks: Map<String, ParsedBlock>,
codeBlocks: Map<String, ParsedBlock>,
testData: ParsedBlock,
response: ParsedBlock
): DynamicNode = DynamicTest.dynamicTest("Integration Test", response.uri) {
val dataFetchingInterceptor = setupDataFetchingInterceptor(testData)
val request = codeBlocks[GRAPHQL_MARKER]?.code()
?: throw IllegalStateException("missing graphql for $title")


val requestParams = codeBlocks[GRAPHQL_VARIABLES_MARKER]?.code()?.parseJsonMap() ?: emptyMap()

val queryContext = codeBlocks[QUERY_CONFIG_MARKER]?.code()
?.let<String, QueryContext?> { config -> return@let MAPPER.readValue(config, QueryContext::class.java) }
?: QueryContext()


val schema = createSchema(globalBlocks, codeBlocks, dataFetchingInterceptor)
val graphql = GraphQL.newGraphQL(schema).build()
val result = graphql.execute(ExecutionInput.newExecutionInput()
.query(request)
.variables(requestParams)
.context(queryContext)
.build())
Assertions.assertThat(result.errors).isEmpty()

val values = result?.getData<Any>()

if (response.code.isEmpty()) {
val actualCode = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(values)
response.adjustedCode = actualCode
} else {
val expected = fixNumbers(response.code().parseJsonMap())
val actual = fixNumber(values)
if (!Objects.equals(expected, actual)) {
val actualCode = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(values)
response.adjustedCode = actualCode
} else {
val expected = fixNumbers(response.code().parseJsonMap())
val actual = fixNumber(values)
if (!Objects.equals(expected, actual)) {
val actualCode = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(values)
response.adjustedCode = actualCode
}
Assertions.assertThat(actual).isEqualTo(expected)
}
Assertions.assertThat(actual).isEqualTo(expected)
}
}

Expand Down
8 changes: 4 additions & 4 deletions core/src/test/resources/filter-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3206,7 +3206,7 @@ RETURN p {
.GraphQL-Query
[source,graphql]
----
{ p: company { employees(filter: { OR: [{ name: "Jane" },{name:"Joe"}]}) { name }}}
{ p: company { employees(filter: { OR: [{ name: "Jane" },{name:"Joe"}]}, orderBy: name_desc) { name }}}
----

.GraphQL-Response
Expand Down Expand Up @@ -3239,10 +3239,10 @@ RETURN p {
----
MATCH (p:Company)
RETURN p {
employees: [(p)<-[:WORKS_AT]-(pEmployees:Person) WHERE (pEmployees.name = $filterPEmployeesOr1Name
OR pEmployees.name = $filterPEmployeesOr2Name) | pEmployees {
employees: apoc.coll.sortMulti([(p)<-[:WORKS_AT]-(pEmployees:Person) WHERE (pEmployees.name = $filterPEmployeesOr1Name
OR pEmployees.name = $filterPEmployeesOr2Name) | pEmployees {
.name
}]
}], ['name'])
} AS p
----

Expand Down
2 changes: 1 addition & 1 deletion core/src/test/resources/issues/gh-112.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ CREATE
.GraphQL-Query
[source,graphql]
----
query {
query user( $uuid: ID ){
user(uuid: $uuid) {
uuid
name
Expand Down
14 changes: 11 additions & 3 deletions core/src/test/resources/issues/gh-147.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ query {
person(name: "Kevin Bacon") {
born
... on Actor {
__typename
namedColleagues(name: "Meg") {
__typename
... name
}
}
Expand All @@ -75,11 +77,13 @@ fragment name on Actor { name }
----
{
"person" : [ {
"born" : 1958,
"__typename" : "Actor",
"namedColleagues" : [ {
"__typename" : "Actor",
"name" : "Meg Ryan"
} ],
"score" : 7,
"born" : 1958
"score" : 7
} ]
}
----
Expand All @@ -90,7 +94,9 @@ fragment name on Actor { name }
{
"personName" : "Kevin Bacon",
"personNamedColleaguesName" : "Meg",
"personScoreValue" : 7
"personNamedColleaguesValidTypes" : [ "Actor" ],
"personScoreValue" : 7,
"personValidTypes" : [ "Actor" ]
}
----

Expand All @@ -101,10 +107,12 @@ MATCH (person:Person)
WHERE person.name = $personName
RETURN person {
.born,
__typename: head([label IN labels(person) WHERE label IN $personValidTypes]),
namedColleagues: [personNamedColleagues IN apoc.cypher.runFirstColumnMany('WITH $this AS this, $name AS name WITH $this AS this MATCH (this)-[:ACTED_IN]->()<-[:ACTED_IN]-(other) WHERE other.name CONTAINS $name RETURN other', {
this: person,
name: $personNamedColleaguesName
}) | personNamedColleagues {
__typename: head([label IN labels(personNamedColleagues) WHERE label IN $personNamedColleaguesValidTypes]),
.name
}],
score: apoc.cypher.runFirstColumnSingle('WITH $this AS this, $value AS value RETURN $value', {
Expand Down

0 comments on commit a92b8ca

Please sign in to comment.