Testing
Causet applications are tested at three levels:
- Compiler validation — syntax and cross-reference checks on every PR
- Integration tests — submit intents, assert projection state
- Regression tests — replay historical events against a new IR version
There is no unit test framework for the DSL — the declarative nature of the language makes integration testing the primary quality gate.
Level 1: Compiler Validation (CI)
Run the compiler on every pull request. A non-zero exit code means the DSL has errors and should block merge.
# In CI
causet build compile --runtime ./my-app --out ./build/outThis catches:
- Undefined field references
- Invalid operation names
- Non-deterministic expressions
- Event type mismatches
- Missing required fields
GitHub Actions example:
name: Compile Causet DSL
on: [push, pull_request]
jobs:
compile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
- name: Compile DSL
run: |
causet build compile --runtime ./my-app --out ./build/out
- name: Upload IR artifacts
uses: actions/upload-artifact@v4
with:
name: causet-ir
path: ./build/out/Level 2: Integration Tests
Integration tests run against a live local stack and are the primary way to verify that your application behaves correctly end-to-end.
Setup
# Start the local stack
docker compose up -d
# Wait for services to be healthy
causet doctor --env local # or check health endpoints manually
# Deploy your app to a test fork
# Via control plane UI or APITest Structure
tests/
integration/
follow-artist.test.ts
announce-show.test.ts
ticket-purchase-saga.test.tsExample Test (Node.js/Vitest + @causet/sdk-node)
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createCausetClient } from '@causet/sdk-node';
const PLATFORM = 'test-platform';
const APP = 'concert-app';
const FORK = `test-${Date.now()}`;
let client: ReturnType<typeof createCausetClient>;
beforeAll(async () => {
client = createCausetClient({
apiUrl: process.env.CAUSET_API_URL ?? 'http://localhost:8085',
platformSlug: PLATFORM,
appSlug: APP,
forkId: FORK,
apiKey: process.env.CAUSET_API_KEY,
});
await client.init();
});
afterAll(() => client.destroy());
describe('FOLLOW_ARTIST', () => {
it('creates a user_following row', async () => {
const result = await client.submitIntent('user_stream', 'user-1', 'FOLLOW_ARTIST', {
user_id: 'user-1',
artist_id: 'artist-1',
});
expect(result.accepted).toBe(true);
// Wait for projection materialization
await new Promise((r) => setTimeout(r, 500));
const { items } = await client.runQuery('user_following_for_user', {
user_id: 'user-1',
});
expect(items).toHaveLength(1);
expect(items[0].artist_id).toBe('artist-1');
});
it('rejects self-follow', async () => {
const result = await client.submitIntent('user_stream', 'user-1', 'FOLLOW_ARTIST', {
user_id: 'user-1',
artist_id: 'user-1',
});
expect(result.accepted).toBe(false);
});
});Test Isolation
Option 1 — separate fork per test run (recommended):
Create a unique fork ID for each CI run. Each fork gets its own tenant schema, preventing test data from leaking between runs.
const FORK = `test-${process.env.CI_RUN_ID ?? Date.now()}`;The fork schema is created on first deployment:
causet fork create "$TEST_FORK" --parent main
causet deploy --fork "$TEST_FORK" --runtime .causet migrations apply (applying DDL independently of a deploy) is proposed and has no CLI equivalent today — DDL is applied automatically as part of causet deploy.
Option 2 — truncate tables between tests:
TRUNCATE TABLE test_platform_concert_app_main.user_following CASCADE;
TRUNCATE TABLE test_platform_concert_app_main.artist_show_directory CASCADE;
-- ...This is faster than creating a new fork but risks cross-test contamination if tests run in parallel.
Level 3: Regression Testing (Replay)
When changing a projection’s derive: expressions or adding new fields, replay historical events against the new IR to verify the output matches expectations.
Approach:
- Capture a snapshot of production projection table (or a fixture dataset)
- Deploy the new IR to a test fork
- Replay events from
causet.ledger-events.v1into the test fork - Compare output against the expected snapshot
# Replay to test fork (sandbox-only)
causet recovery replay --stream user_stream --fork regression-test --from-cursor 0causet replay scoped to a single named projection (shown elsewhere in this section) is proposed; causet recovery replay above is the real, shipped, stream-scoped equivalent.
Then query the test fork and assert the results match the expected values.
Proposed Test Utilities
Proposed: The following test utilities are on the roadmap.
causet test setup
Creates a clean test fork and applies the current IR:
causet test setup \
--platform test-platform \
--app concert-app \
--fork $TEST_FORKcauset test teardown
Drops the test fork schema:
causet test teardown \
--platform test-platform \
--app concert-app \
--fork $TEST_FORKProjection Fixtures
Define fixture events in the DSL for each projection. The compiler validates that the fixture events produce the expected projection output:
# projections/user_following.projections.causet
projections:
user_following:
...
fixtures:
- event_type: ARTIST_FOLLOWED
payload:
user_id: "user-test"
artist_id: "artist-test"
expect:
- user_id: "user-test"
artist_id: "artist-test"A causet projections doctor command that validates fixtures before deployment is proposed but not yet implemented; causet build validate is the real, shipped compile-time check.
What Not to Test
- DSL expressions in isolation — you cannot unit test a SpEL expression outside the runtime. Test the behavior through intent submission + query.
- Projection SQL — the compiler generates the SQL from your
derive:andfields:blocks. Trust the compiler; test the end-to-end behavior. - Event ordering — per-entity ordering is guaranteed by the runtime. You do not need to test this.