Skip to content

Integration Testing

The JSONQL ecosystem validates every SDK through a language-agnostic integration test harness. A single Python-based test runner sends HTTP requests to adapter servers in any language and asserts consistent behavior across all combinations of SDK, framework, and database.

graph TD
Runner["pytest runner<br/>(Python)"]
Provision["Provisioner<br/>DDL + seed data"]
Server["Adapter Server<br/>(any language)"]
SDK["JSONQL SDK"]
DB["Database"]
Runner -->|1. provision| Provision
Provision -->|2. DDL + seed| DB
Runner -->|3. HTTP request| Server
Server -->|4. parse + transpile| SDK
SDK -->|5. execute| DB
DB -->|6. result| SDK
SDK -->|7. hydrate| Server
Server -->|8. JSON response| Runner
Runner -->|9. assert| Runner

The test runner is completely decoupled from the SDK — it only speaks HTTP. This means any language can be tested by implementing a thin adapter server that delegates to the SDK.

Every adapter server must implement the following HTTP contract.

GET /health → 200 {"status": "ok"}

Used by Docker health checks and run_tests.sh readiness polling.

POST /{table}
Content-Type: application/json
{
"fields": ["id", "name"],
"where": { "status": { "eq": "active" } },
"sort": "-created_at",
"limit": 10
}

Response:

{ "data": [{ "id": 1, "name": "Alice" }] }
MethodPathBodyStatusOperation
POST/{table}{"data": {"name": "Alice"}}201Create
PATCH/{table}?id=X{"data": {"name": "Updated"}}200Update
DELETE/{table}?id=X200Delete
{ "error": "Invalid query", "details": "Unknown field: foo" }

Status codes: 400 (bad request), 403 (forbidden by hook), 500 (server error).

The test runner injects headers to control server behavior. Servers must read and honor these:

HeaderPurposeUsed By
X-JSONQL-Test-PathPoints to the test suite directory for schema resolutionAll servers
X-Test-RoleSimulates RLS — user restricts results to role = "user"Lifecycle
X-Test-TransformResponse transform — upper uppercases all name fieldsLifecycle
X-Test-Hook-FailForces a hook to fail — beforeCreate, beforeUpdate, beforeDeleteLifecycle
X-Test-Hook-ModifyForces a hook to modify data — beforeCreate, beforeUpdateLifecycle
X-Test-Hook-AfterUpdatemark → adds updated: true to responseLifecycle
X-Test-Hook-AfterDeleteconfirm → returns {"deleted": true, "id": X}Lifecycle
X-Test-Parser-MaxLimitEnforces maximum query limitSimple
X-Test-Parser-AllowedFieldsRestricts selectable fields (comma-separated)Simple
X-Test-Parser-AllowedIncludesRestricts includeable relations (comma-separated)Simple
X-Test-Validatestrict → rejects unknown fields in querySimple
  • Test query parsing, filtering, sorting, pagination, includes, aggregation, field permissions, and error handling
  • Use the full 8-table schema with field-level access control
  • Support all 5 databases (PostgreSQL, MySQL, SQLite, MSSQL, MongoDB)
  • Wire minimal hooks: schemaResolver for per-suite schema overrides, beforeQuery for parser option enforcement
  • Run test suites: basic/, advanced/, errors/, validation/, parsing/, parser-options/, features-v1-1/, misc/
  • Test mutation hooks, RLS simulation, response transformation, and CUD operations
  • Use a minimal inline schema — only users table with id, name, role, score
  • PostgreSQL only
  • Wire all 8 lifecycle hooks (before/afterQuery, before/afterCreate, before/afterUpdate, before/afterDelete)
  • Run test suite: lifecycle/ exclusively
VariableDescriptionDefault
PORTHTTP listen port8080
DB_TYPEDatabase dialect: postgres, mysql, sqlite, mssql, mongodbpostgres
DB_DSNConnection string (URI format)varies
JSONQL_SCHEMA_PATHPath to schema JSON file../../fixtures/standard/schema.json
VariableDatabaseDescription
DB_DSNPostgreSQLpostgresql://jsonql:password@localhost:5432/jsonql_test
DB_DSNMySQLmysql://jsonql:password@localhost:3306/jsonql_test
DB_FILENAMESQLitePath to .db file (e.g., data/sqlite.db)
DB_DSNMongoDBmongodb://localhost:27017
DB_NAMEMongoDBDatabase name (default: jsonql_test)

Java Spring Boot servers additionally use standard Spring properties:

VariableDescription
SPRING_DATASOURCE_URLJDBC connection URL
SPRING_DATASOURCE_USERNAMEDatabase username
SPRING_DATASOURCE_PASSWORDDatabase password

All servers implement a two-tier schema resolution pattern:

  1. Startup: Load the default schema from JSONQL_SCHEMA_PATH
  2. Per-request: Check the X-JSONQL-Test-Path header
    • If set, look for schema.json in that directory
    • If found, use the suite-specific schema (results are cached)
    • If not found, fall back to the default schema

Lifecycle servers skip schema resolution — they use a hardcoded inline schema.

The default schema (fixtures/standard/schema.json) defines 8 tables:

TablePurpose
usersPrimary entity with id, name, email, role, age
postsHas user_id FK → users (hasMany relationship)
commentsHas post_id FK → posts (hasMany relationship)
productsStandalone catalog entity
ordersHas user_id FK → users
widgetsHas JSONB metadata field (PostgreSQL-specific)
restricted_usersField-level access control (allowSelect, allowFilter, allowSort)
secretsRestricted relation testing

Lifecycle servers implement 8 hooks that modify query/mutation behavior based on test headers:

X-Test-Role: user → Inject WHERE role = "user" (RLS simulation)
X-Test-Transform: upper → Uppercase all "name" fields in response
  • X-Test-Hook-Fail: beforeCreate → Return 400
  • X-Test-Hook-Modify: beforeCreate → Rename OriginalName to ModifiedName
  • Auto-generates id if not provided
  • If modified, returns {"data": {"name": "ModifiedName"}}
  • X-Test-Hook-Fail: beforeUpdate → Return 403
  • X-Test-Hook-Modify: beforeUpdate → Set score = 999
  • If modified, returns patched score
  • X-Test-Hook-AfterUpdate: mark → Adds updated: true to response
  • X-Test-Hook-Fail: beforeDelete → Return 403
  • X-Test-Hook-AfterDelete: confirm → Returns {"deleted": true, "id": X}

Each language has shared helpers that centralize compliance boilerplate:

LanguageLocationKey Exports
TypeScriptintegration-tests/shared/loadDefaultSchema(), createSchemaResolver(), enforceTestHeaders(), lifecycleHooks
Gointegration-tests/gohelpers/NewSimpleEnv(), NewLifecycleEnv(), LifecycleHandler(), HealthHandler()
Pythonintegration-tests/py-shared/build_simple_options(), build_simple_mongo_options(), build_lifecycle_options(), enforce_parser_options()
Javaintegration-tests/java-shared/ComplianceRequestHandler, ComplianceHelpers, SimpleComplianceLifecycle, FullComplianceLifecycle

Thanks to the shared helpers, adapter servers are very small:

ServerLines of Code
TypeScript Express (simple)~40
Go Gin (simple)~35
Python Flask (simple)~35
Java Spring Boot (simple)~60
TypeScript Express (lifecycle)~70
Go Gin (lifecycle)~25
Python Flask (lifecycle)~30
Java Spring Boot (lifecycle)~60

The base docker-compose.yml defines four database services:

ServiceImagePort
postgrespostgres:155432
mysql_dbmysql:8.03306
mssql_dbmcr.microsoft.com/azure-sql-edge:latest1433
mongodbmongo:727017

All databases have health checks. SQLite uses a local file (no container needed).

Each language has its own Docker Compose overlay:

FileLanguageBase ImageStart Command
docker-compose.ts.ymlTypeScriptnode:18npm install && npm start
docker-compose.go.ymlGogolang:1.24go mod tidy && go run main.go
docker-compose.java.ymlJavamaven:3.9-eclipse-temurin-17mvn spring-boot:run or mvn package && java -jar …-microbundle.jar
docker-compose.py.ymlPythonpython:3.12-slimpip install -r requirements.txt && python server.py
Port RangeLanguageType
8080–8089TypeScriptExpress / Fastify / NestJS — simple
8090–8098Gonet/http / Gin / Echo — simple
8099–8105JavaSpring Boot / Jakarta EE — simple
8106–8114PythonFlask / FastAPI / Django — simple
8115–8125All languagesMSSQL variants
8127–8128TypeScriptLifecycle servers
8129–8136Go / Java / PythonMongoDB variants
8137–8139GoLifecycle servers
8140–8141JavaLifecycle servers
8142–8144PythonLifecycle servers

Docker service names follow the pattern:

{lang}-{framework}-{type}-{db}

Examples: ts-express-simple-pg, go-gin-lifecycle-pg, java-spring-boot-simple-mssql, py-flask-simple-mongodb

Located in fixtures/standard/:

FileContent
schema.json8-table schema with relationships and field permissions
data.jsonSeed data: 3 users, 3 posts, 3 comments, 4 products, 2 orders, 3 widgets, 2 restricted_users, 2 secrets
ddl/postgres.sqlPostgreSQL DDL (SERIAL, VARCHAR, DECIMAL, JSONB, BOOLEAN)
ddl/mysql.sqlMySQL DDL equivalent
ddl/sqlite.sqlSQLite DDL equivalent
ddl/mssql.sqlMSSQL DDL (NVARCHAR, BIT, IDENTITY)
ddl/mongo_seed.jsMongoDB collection creation + indexes

Located in tests/unified/lifecycle/fixtures/:

  • Only the users table — 2 rows: Alice/admin/100, Bob/user/50
  • Each test case modifies this data through hooks

The provisioner (runners/provisioner.py) is a CLI tool that resets databases before each test batch:

  1. Drop all existing tables
  2. Apply DDL from the suite-specific SQL file
  3. Seed data from JSON
  4. Reset sequences (PostgreSQL setval, MySQL auto-increment, MSSQL IDENTITY_INSERT)

Supports all 5 database types with retry logic (10–15 attempts) for connection establishment.

Terminal window
python3 runners/provisioner.py \
--type postgres \
--db-dsn "postgresql://jsonql:password@localhost:5432/jsonql_test" \
--ddl fixtures/standard/ddl/postgres.sql \
--data fixtures/standard/data.json

To add compliance testing for a new framework:

integration-tests/{lang}-{framework}-simple/
├── server.{ext} # Main entry point
├── package.json # (TS) or go.mod (Go) or requirements.txt (Py) or pom.xml (Java)
└── ...

Your server must:

  1. Read PORT, DB_TYPE, DB_DSN, and JSONQL_SCHEMA_PATH from environment
  2. Expose GET /health{"status": "ok"}
  3. Expose POST /{table} that:
    • Reads the JSON body as a JSONQL query
    • Reads the X-JSONQL-Test-Path header for schema resolution
    • Parses, validates, transpiles, executes, and hydrates via the SDK
    • Returns {"data": [...]} on success
    • Returns {"error": "...", "details": "..."} on failure
  4. Honor the X-Test-Parser-* and X-Test-Validate headers

Use the language-specific shared helpers to minimize boilerplate.

Add a service to the appropriate docker-compose.{lang}.yml:

{lang}-{framework}-simple-pg:
build:
context: ../jsonql-{lang}
working_dir: /app/integration-tests/{lang}-{framework}-simple
ports:
- "{port}:8080"
environment:
DB_TYPE: postgres
DB_DSN: postgresql://jsonql:password@postgres:5432/jsonql_test
JSONQL_SCHEMA_PATH: ../../fixtures/standard/schema.json
depends_on:
postgres:
condition: service_healthy

Add an entry to the appropriate *_TESTS array:

Terminal window
"{lang}-{framework}-simple-pg:{port}:postgres:--db-dsn postgresql://jsonql:password@localhost:5432/jsonql_test"
Terminal window
./run_tests.sh --filter {framework}

All 135 tests must pass.

github.com/jsonql-standard/jsonql-tests