This guide answers "which tier does my test belong to, where does the file go, and what should it assert?" Four tiers cover every test in the rewrite. For design principles (why pipeline is the primary behavioural tier, why code-string body matching is banned), see Rewrite Design Principles. For build commands and database setup, see .claude/web-environment.md.

Choosing a tier

Read top-down; stop at the first match.

  1. The behaviour is "this generated source must compile against the real jOOQ catalog"Compilation. No test class to write; the fixture-driven mvn compile -pl :graphitron-sakila-example -Plocal-db is the assertion. Add a fixture instead of an assertion.

  2. The behaviour is "this generated request must round-trip against PostgreSQL and return the right rows / fire the right number of queries / honour DataLoader batching"Execution. New @Test in GraphQLQueryTest (or one of the federation-/scatter-named companions).

  3. The behaviour is "this SDL pattern classifies into this variant" or "this SDL pattern produces a TypeSpec with this method shape"Pipeline. New case in GraphitronSchemaBuilderTest (classification truth table) or a new *PipelineTest file.

  4. The behaviour is "this builder helper / classifier method / writer primitive / validator rule does X on input Y"Unit. New case in a *Test next to the production class, or a *ValidationTest for validator rules.

When two tiers could apply, prefer the one that captures the behaviour most directly. Pipeline beats unit: per-variant structural tests are bookkeeping; the primary signal is that a realistic SDL produces a realistic TypeSpec end-to-end. Pipeline also beats compilation and execution where the behaviour can be asserted on the classified model or TypeSpec shape, since pipeline runs without jOOQ codegen or Postgres. Execution beats compilation only when SQL behaviour or row content is the contract.

Tier is determined by what’s asserted, not by what module the file lives in. graphitron-sakila-example hosts tests at every tier; its module dependency on post-generator artifacts is the reason those tests live there, not a tier signal.

Tier annotations

Each tier has a JUnit 5 meta-annotation in graphitron’s test source root (`no.sikt.graphitron.rewrite.test.tier), republished as a tests test-jar so other modules can consume them. Reachable from every test class in graphitron and graphitron-sakila-example:

@UnitTier        // @Tag("unit")
@PipelineTier    // @Tag("pipeline")
@CompilationTier // @Tag("compilation")
@ExecutionTier   // @Tag("execution")

Place exactly one annotation at the class level. Tests that don’t fit any of the four tiers (GeneratorDeterminismTest is the only current example) carry @Tag("cross-cutting") directly.

With class-level tags in place, mvn test -Dgroups=pipeline runs only pipeline-tier classes; -DexcludedGroups=execution skips Postgres for fast inner loops. Both Surefire and Failsafe honour these flags without further config.

An enforcement test in each in-scope module (graphitron, graphitron-sakila-example) walks that module’s own test classpath and fails the build if any @Test-bearing class lacks a tier identity, or carries more than one.

Unit tier

Structural invariants on individual classifiers, builders, emitters, and runtime helpers.

Where: graphitron/src/test/java/…​ next to the production class.

Three sub-families:

Generator unit tests (TypeFetcherGeneratorTest, TypeClassGeneratorTest, TypeConditionsGeneratorTest, GeneratorCoverageTest; and the generators/schema/ subdirectory: EnumTypeGeneratorTest, GraphitronFacadeGeneratorTest, InputTypeGeneratorTest, ObjectTypeGeneratorTest, etc.). Take pre-built model fixtures via TestFixtures; assert TypeSpec shape (method names, return types, parameter signatures). Banned: code-string body matching on the generated MethodSpec body; that is what compilation and execution cover.

Validator unit tests (*ValidationTest family, e.g. ColumnFieldValidationTest, QueryTableFieldValidationTest). Build a GraphitronSchema with one parent type and one field at a known coordinate; assert validate() outcomes by RejectionKind and message substring.

Builder / catalog / writer unit tests (JooqCatalogFindColumnTest, IdempotentWriterTest, ArgBindingMapTest, ServiceCatalogTest, etc.). Targeted constructor or single-method assertions; no full-pipeline plumbing.

Pipeline tier

SDL → classified model → generated TypeSpec.

Where: graphitron/src/test/java/no/sikt/graphitron/rewrite/.

Two shapes:

Classification truth tables: GraphitronSchemaBuilderTest. Each variant family is a // ===== VariantName ===== section with an enum where each constant is one (description, SDL, assertion) triple; one parameterised test iterates the table.

Deeper SDL → TypeSpec / variant-shape tests: *PipelineTest files: NodeIdPipelineTest, SplitTableFieldPipelineTest, TableFieldPipelineTest, LookupTableFieldPipelineTest, NestingFieldPipelineTest, ServiceRootFetcherPipelineTest, TaggedInputsPipelineTest, StubbedVariantPipelineTest; and in generators/: FetcherPipelineTest, TablePipelineTest. Build a schema with TestSchemaHelper.buildSchema(sdl), assert structural shape on the resulting variant or generated TypeSpec. Banned: code-string body matching.

Compilation tier

Generated source must compile against the test catalog.

Where: graphitron-sakila-example, run with mvn compile -f graphitron-rewrite/pom.xml -pl :graphitron-sakila-example -Plocal-db. The compiler is the assertion; no hand-written assertions are needed for type correctness.

Two test classes layer structural checks on top:

GeneratedSourcesSmokeTest: every expected class is present in the emitter’s output package (catches a generator that silently drops a class).

GeneratedSourcesLintTest: generator-hygiene rules over emitted source text (e.g. no var in emitted code).

Execution tier

Full GraphQL request → SQL → row round-trip.

Where: graphitron-sakila-example, run with mvn test -f graphitron-rewrite/pom.xml -pl :graphitron-sakila-example -Plocal-db.

Canonical classes: GraphQLQueryTest on the shared fixture; FederationEntitiesDispatchTest on the federated fixture.

Patterns:

  • JDBC round-trip count via the QUERY_COUNT listener (AtomicInteger reset per test to assert DataLoader batching or lazy-on-selection).

  • Returned-row-id sets and field-value assertions against the Sakila fixture catalog.

  • Structural SQL-shape assertions via the SQL_LOG ExecuteListener (e.g. that no select count ran when totalCount was not selected).

Module location vs. tier (graphitron-sakila-example)

Several tests live in graphitron-sakila-example for module-dependency reasons but classify by assertion, not module. Only GeneratorDeterminismTest is @Tag("cross-cutting"); the rest carry one of the four tier annotations:

  • GeneratedSourcesSmokeTest, GeneratedSourcesLintTest: @CompilationTier (consume the compile output).

  • FederationBuildSmokeTest, NoFederationRegressionTest: @PipelineTier (schema-construction assertions on the fixture-derived generated facade; no SQL).

  • ScatterSingleByIdxTest: @UnitTier (direct unit coverage, fully in-memory; lives in graphitron-sakila-example because it reflects against a generated *Fetchers class).

  • GraphQLQueryTest, FederationEntitiesDispatchTest: @ExecutionTier.

  • GeneratorDeterminismTest: @Tag("cross-cutting"), system-level ratchet for the three-clause writer contract (determinism + minimal-change writes + clean orphan removal). Does not fit pipeline (no classifier-to-TypeSpec assertion), compilation (no compile happens), or execution (no SQL).

Build commands

# Unit + pipeline (no database needed)
mvn test -f graphitron-rewrite/pom.xml -pl :graphitron -Plocal-db

# Compilation (generated source compiles against real jOOQ catalog)
mvn compile -f graphitron-rewrite/pom.xml -pl :graphitron-sakila-example -Plocal-db

# All tiers including execution (requires local PostgreSQL via -Plocal-db)
mvn test -f graphitron-rewrite/pom.xml -Plocal-db

# Skip execution tier for fast inner loops
mvn test -f graphitron-rewrite/pom.xml -pl :graphitron -Plocal-db -DexcludedGroups=execution

See .claude/web-environment.md for database setup prerequisites and the fixtures-jar footgun recovery.