Test Support

Because the rich schema exposes pretty much all the behaviour of the domain model, it’s possible to use GraphQL queries as a way to write end-to-end tests. This works especially well with approval tests, which can be used to efficiently assert the contents of the returned response.

When writing tests, it can be useful to first generate the schema. You can then register the schema with a dedicated editor; this can then provide code-completion. Some IDEs (for example IntelliJ IDEA) also provide this capability.

Subclass the PrintSchemaIntegTestAbstract and optionally specify the API variant. For example:

import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;

import org.apache.causeway.viewer.graphql.viewer.testsupport.schema.PrintSchemaIntegTestAbstract;

import static org.apache.causeway.core.config.CausewayConfiguration.Viewer.Graphql.ApiVariant;

@Import({
        UniversityModule.class                                      (1)
})
public class PrintSchemaIntegTest
    extends PrintSchemaIntegTestAbstract {                          (2)

    @DynamicPropertySource
    static void apiVariant(DynamicPropertyRegistry registry) {      (3)
        registry.add("causeway.viewer.graphql.api-variant",
            ApiVariant.QUERY_WITH_MUTATIONS_NON_SPEC_COMPLIANT::name);
    }
}
1 specifies the domain module(s) to test
2 subclass from the superclass
3 optionally specify the API variant. You could also use application-test.properties or application-test.yml

This will then write out a schema.gql file into src/test/resources of the maven module containing the test class.

Integration Test Support

The CausewayViewerGraphqlIntegTestAbstract class is intended to be used as the base class for your integration tests. It spins up an instance of the app (listening on a random port), and then allows you to submit queries and mutations against the backend.

Since most tests consist of a query and then checking a response, the superclass also provides some sophisticated support here. All you need to do is to save your queries in the same package as your subclass. These are automatically picked by the test class, submitted, and then the approval tests assertion library is used to compare the results.

The first time you run the tests, they will of course fail because you will be missing the "approved" responses. Check that the received response is as expect, and then approve it.

To use it subclass it, use @Import to specify the modules to be tested, and then save queries or mutations as files in the same package.

For example:

Calculator.java
@Named("calculator.Calculator")
@DomainService
@RequiredArgsConstructor(onConstructor_ = {@Inject})
public class Calculator {

    @Action(semantics = SemanticsOf.SAFE)
    public boolean and(boolean x, boolean y) {
        return x & y;
    }

    @Action(semantics = SemanticsOf.SAFE)
    public String concat( String prefix, String suffix) {
        return prefix + suffix;
    }
}

To test this service, we would use:

Calculator_IntegTest.java
import org.apache.causeway.viewer.graphql.viewer.testsupport.PrintSchemaIntegTestAbstract;

@Import({
        CalculatorModule.class                              (1)
})
public class Calculator_IntegTest
    extends CausewayViewerGraphqlIntegTestAbstract {        (2)

    protected Calculator_IntegTest() {
        super(Calculator_IntegTest.class);                  (3)
    }

    @Override
    @TestFactory
    public Iterable<DynamicTest> each() throws Exception {  (4)
        return super.each();
    }
}
1 specifies the domain module(s) to test
2 subclass from the superclass
3 indicates which package to pick up the queries
4 dynamically generates tests for each query

Then include the following tests in the same package, using the naming convention:

class_name.each.anything_you_want._.gql

The word "each" comes from the name of the overridden method. If you want to change the "_.gql" suffix, you can pass an additional argument in the constructor.

For example:

  • to exhaustively test the Calculator#and() action:

    Calculator_IntegTest.each.and_1._.gql
    {
      rich {
        university_calc_Calculator {
          and {
            invoke(x: true, y: true) {
              results
            }
          }
        }
      }
    }
    Calculator_IntegTest.each.and_2._.gql
    {
      rich {
        university_calc_Calculator {
          and {
            invoke(x: true, y: false) {
              results
            }
          }
        }
      }
    }
    Calculator_IntegTest.each.and_3._.gql
    {
      rich {
        university_calc_Calculator {
          and {
            invoke(x: false, y: true) {
              results
            }
          }
        }
      }
    }
    Calculator_IntegTest.each.and_4._.gql
    {
      rich {
        university_calc_Calculator {
          and {
            invoke(x: false, y: false) {
              results
            }
          }
        }
      }
    }
  • or, to test the concat action:

    Calculator_IntegTest.each.and_1._.gql
    {
      rich {
        university_calc_Calculator {
          concat {
            invoke(prefix: "Fizz", suffix: "Buzz") {
              results
            }
          }
        }
      }
    }

For further examples, take a look at the tests for the GraphQL viewer itself, which use this class extensively.

Scenario Tests

The GraphQL viewer also supports what we call scenario tests. This is an extension to the rich schema, to include a new field “Scenario” which in turn can have three further fields, “Given”, “When” and “Then”. The scenario can also be named.

Moreover, scenario test enables introduces a "saveAs" capability (within the meta field) to tag objects and then use them at a later stage.

This is all probably most easily explained with an example:

{
  rich {
    Scenario(name: "Find department and change its name"){    (1)
      Name                                                    (1)

      Given {                                                 (2)
        university_dept_Departments {
          findDepartmentByName {
            invoke(name: "Classics") {
              args {
                name
              }
              results {
                _meta {
                  saveAs(ref: "dept#1")                       (3)
                }
              }
            }
          }
        }
      }

      When {                                                  (4)
        university_dept_Department(
            object: {ref: "dept#1"}                           (5)
        ) {
          name {
            get
          }
          changeName {
            invokeIdempotent(newName: "Ancient History") {
              args {
                newName                                       (6)
              }
              results {                                       (7)
                name {
                  get
                }
              }
            }
          }
        }
      }

      Then {
        university_dept_Department(object: {ref: "dept#1"}) {
          name {
            get                                               (8)
          }
        }
      }
    }
  }
}
1 We name the scenario. The Name field means that the scenario’s name will be printed out in the response.
2 Under the Given field, we set up or locate the objects that are to be interacted with within the scenario.
3 Having found an object, we save it with some meaningful name.
4 Under the When field, we interact with the object.
5 We use the tag from before to locate the object
6 We request to print out the arguments
7 The object is changed
8 We assert on the results

This will result in a response such as:

{
  "data" : {
    "rich" : {
      "Scenario" : {
        "Name" : "Find department and change its name",
        "Given" : {
          "university_dept_Departments" : {
            "findDepartmentByName" : {
              "invoke" : {
                "args" : {
                  "name" : "Classics"
                },
                "results" : {
                  "_meta" : {
                    "saveAs" : "dept#1  "
                  }
                }
              }
            }
          }
        },
        "When" : {
          "university_dept_Department" : {
            "name": {
              "get": "Classics"
            },
            "changeName" : {
              "invokeIdempotent" : {
                "args" : {
                  "newName" : "Ancient History"
                },
                "results" : {
                  "name" : {
                    "get" : "Ancient History"
                  }
                }
              }
            }
          }
        },
        "Then" : {
          "university_dept_Department" : {
            "name" : {
              "get" : "Ancient History"
            }
          }
        }
      }
    }
  }
}

In this case you’ll notice that the Then field doesn’t actually show any additional assertions to those already in the When clause, so it could have been omitted.

Testing support is enabled with the causeway.viewer.graphql.schema.rich.enable-scenario-testing configuration property.