Skip to main content

Testing

The project uses Vitest across the entire monorepo. Tests are split into three categories: unit tests, frontend integration tests, and backend integration tests against real ClickHouse instances via testcontainers.

Test categories

Unit tests

Pure logic tests with no external dependencies. These run instantly and cover things like SQL template resolution, query literal escaping, and pure computation helpers.

packages/core/src/__tests__/cluster-service.test.ts
packages/core/src/__tests__/query-literals.test.ts

Run them with:

just test-core

Frontend integration tests

Verify that the main app's Zustand stores correctly call shared services through mock adapters, and that shared UI components (@tracehouse/ui-shared) are importable and type-compatible in the main app context.

frontend/src/__tests__/integration/main-app-migration.test.tsx

These use mock adapters that return predetermined rows for specific SQL patterns, so they don't need Docker or a running ClickHouse instance.

just test-frontend

Backend integration tests (testcontainers)

This is the most important test layer. Integration tests in packages/core/src/__tests__/integration/ run against a real ClickHouse instance - either a testcontainer (default) or an external server you provide.

Each test file spins up a fresh ClickHouse Docker container, creates databases and tables, inserts data to trigger real merges/mutations, and validates the service layer against actual system tables. Everything is torn down automatically after the test.

Current integration test coverage:

Test fileWhat it validates
merge-trackerActive merges, merge history, mutations, background pool metrics
merge-history-enrichmentMerge category classification across all merge types
merge-lineage-bytesByte-level lineage tracking through merge chains
database-explorerDatabase/table/part listing against real system tables
query-analyzerQuery monitoring and history from system.processes / query_log
metrics-collectorMetric collection from system.metric_log
lineage-builderPart lineage graph construction from part_log
cluster-dedupDeduplication logic on a 2-node replicated cluster
overview-merge-typeMerge type breakdown for the cluster overview
http-adapterLow-level HTTP adapter against the container
connection-managerConnection lifecycle management
error-wrappingError classification and wrapping from real ClickHouse errors
real-system-tablesSchema validation against actual system table shapes

Test tags

Every test suite is tagged by domain — what capability it validates, not how it's tested. Tags use Vitest 4.x native { tags: [...] } syntax on top-level describe() blocks.

Available tags

TagWhat it coversExample tests
securitySandbox escapes, SQL injection, privilege escalation, credential accessreadonly-sandbox, builder.property
merge-engineMerge tracking, classification, history, lineage, ETA, samples, algorithmsmerge-tracker, merge-history-enrichment, merge-classification
query-analysisQuery monitoring, timeline, scan efficiency, EXPLAIN parsingquery-analyzer, timeline-service, explain-parser
storageParts, TTL, compression, table efficiency, pruning, storage policiesdatabase-explorer, database-ttl-detection, connection-http-compression, pruning
clusterMulti-node dedup, distributed queries, replica detection, topologycluster-dedup, sampling-setup-cluster
observabilityMetrics, resource monitoring, process sampling, CPU/mem/disk/net, flamegraphsoverview-metrics-collector, sampling-process-history, query-trace-flamegraph, correlation
connectivityConnection management, HTTP adapter, retry/backoff, error wrappingconnection-manager, connection-http-adapter, connectionRetry
analyticsAnalytics query language, dashboards, meta-language, preset queriesmetaLanguage, analytics-preset-smoke
setupSampling setup scripts, schema provisioning, idempotencysampling-setup-script
visualization3D rendering, pipeline graphs, chart data, formattersPartsVisualization, MergeVisualization

Filtering by tag

Run only tests matching a specific domain:

# Single tag
just test-tag security

# Or directly with vitest
cd packages/core && npx vitest run --tags-filter="security"

# Boolean expressions
npx vitest run --tags-filter="merge-engine || storage"
npx vitest run --tags-filter="observability && !connectivity"

Listing tags

just test-list-tags

Adding tags to new tests

Declare the tag in the relevant vitest.config.ts under test.tags, then use it on top-level describe():

describe('my new feature', { tags: ['merge-engine'] }, () => {
it('does something', () => { ... });
});

Test reports

All test runs generate an HTML report and a JSON results file automatically.

Viewing reports

After running tests, open the interactive HTML report:

just test-report

This serves the report at http://localhost:4173 with a searchable, filterable UI showing every suite and test with pass/fail status, durations, and tag information.

Report locations

PackageHTML reportJSON report
packages/core (unit)test-reports/html/index.htmltest-reports/results.json
packages/core (integration)test-reports/integration-html/index.htmltest-reports/integration-results.json
frontendtest-reports/html/index.htmltest-reports/results.json

Reports are gitignored and regenerated on every test run.

Testcontainer infrastructure

Single-node container

Most tests use a single ClickHouse container via @testcontainers/clickhouse. The setup lives in:

packages/core/src/__tests__/integration/setup/
├── clickhouse-container.ts # Start/stop a single ClickHouse container
├── cluster-container.ts # Start/stop a 2-node cluster with Keeper
├── shadow-adapter.ts # Adapter for shadow system tables
├── shadow-tables.ts # Create/seed shadow copies of system tables
├── table-helpers.ts # Helpers to create test databases and tables
└── index.ts # Re-exports everything

The startClickHouse() function pulls the clickhouse/clickhouse-server:26.1-alpine image, starts a container, and returns a context with a ready-to-use ClusterAwareAdapter and raw @clickhouse/client instance.

Cluster container (2-node)

The cluster-dedup test uses a more complex setup: a 2-node ClickHouse cluster with a ClickHouse Keeper node, all on a shared Docker network. This validates cluster-aware features like replicated table deduplication and cross-node query routing.

The topology is: 1 shard × 2 replicas + 1 Keeper node = 3 containers.

Running integration tests

Default (testcontainer)

# All integration tests
just test-core-integration

# Single test file
cd packages/core
npx vitest run src/__tests__/integration/merge-tracker.integration.test.ts

Docker must be running. The container starts automatically, runs the test, and tears down.

Against an external ClickHouse

Set CH_TEST_URL to skip the container and run against your own instance:

CH_TEST_URL=http://localhost:8123 \
npx vitest run src/__tests__/integration/merge-tracker.integration.test.ts

This works with Docker Compose, a K8s port-forward, or any reachable ClickHouse HTTP endpoint.

Keeping test data for UI inspection

Add CH_TEST_KEEP_DATA=1 to skip dropping the test database on teardown. Useful when you want to point the frontend at the same ClickHouse instance and visually verify the UI against known data:

CH_TEST_URL=http://localhost:8123 CH_TEST_KEEP_DATA=1 \
npx vitest run src/__tests__/integration/merge-history-enrichment.integration.test.ts

Clean up manually when done:

DROP DATABASE IF EXISTS merge_enrich_test;

Environment variables

VariableDefaultDescription
CH_TEST_URL(unset - uses testcontainer)ClickHouse HTTP URL for an external instance
CH_TEST_KEEP_DATA0Set to 1 to preserve test databases on teardown

Limitations

The http-adapter and connection-manager tests require a testcontainer (they call container-specific APIs for host/port). They throw a clear error if you run them with CH_TEST_URL.

Writing a new integration test

  1. Import the setup helpers:
import { startClickHouse, stopClickHouse, type TestClickHouseContext } from './setup/index.js';
  1. Start the container in beforeAll, tear down in afterAll:
let ctx: TestClickHouseContext;

beforeAll(async () => {
ctx = await startClickHouse();
// Create your test database, tables, insert data...
}, 120_000); // container startup can take up to 2 minutes

afterAll(async () => {
await ctx.client.command({ query: 'DROP DATABASE IF EXISTS my_test_db' });
await stopClickHouse(ctx);
}, 30_000);
  1. Use ctx.adapter for service-layer calls and ctx.client for raw SQL setup/teardown.

  2. Name the file *.integration.test.ts so it's picked up by the integration test runner.

Data Utils tests (Python)

The tools/data-utils/ package has its own test suite using pytest and testcontainers. These tests validate the table plugin contracts, create/insert operations, and query generation against a real ClickHouse instance.

tools/data-utils/tests/
├── conftest.py # Testcontainers fixture (session-scoped)
└── test_table_plugins.py # Protocol conformance + integration tests

Running

just test-data-utils

Docker must be running — the tests spin up a ClickHouse container automatically.

What they cover

  • Protocol conformance: each dataset plugin satisfies the Dataset protocol
  • Create + insert: tables are created and data is inserted correctly
  • QuerySet validation: each plugin's queries property returns well-formed SQL
  • InsertConfig: frozen dataclass behaviour and defaults

End-to-end tests (Playwright)

Browser-based e2e tests live in packages/e2e/ and use Playwright. They start a real ClickHouse container and the Vite dev server, then drive the full app through Chromium and a mobile viewport to verify real data flows end-to-end.

What they cover

Test fileWhat it validates
smoke.spec.tsApp boot, nav items, route transitions, settings toggles, responsiveness, performance
connection.spec.ts"Add Connection" form flow and "Test Connection" button against real ClickHouse
connected-pages.spec.tsOverview metrics, Engine Internals, Explorer databases, Queries, Analytics — all with real data

The connectedPage fixture injects a ClickHouse connection via localStorage and waits for actual data to arrive (the refresh indicator must show "Just now" or "Xs ago", not "Connecting...") before handing the page to the test. This means tests that pass are genuinely connected and showing real ClickHouse data.

Running

# Run all e2e tests (headless)
just e2e

# Interactive Playwright UI — step through tests, inspect screenshots, replay traces
just e2e-ui

# Watch tests run in a visible browser (500ms between actions by default)
just e2e-headed

# Slower or faster headed runs
just e2e-headed 1000 # 1 second between actions
just e2e-headed 200 # faster but still visible

Or directly with npm:

cd packages/e2e
npx playwright test # headless
npx playwright test --ui # interactive UI
npx playwright test --headed # visible browser (no slowdown)
SLOWMO=500 npx playwright test --headed # visible + slow

Using an external ClickHouse

Set CH_E2E_URL to skip the Docker container and run against an existing instance:

CH_E2E_URL=http://localhost:8123 just e2e

Infrastructure

The e2e setup follows the same pattern as core integration tests:

  • Global setup (tests/global-setup.ts): starts a ClickHouse container with docker run, waits for health, runs the init SQL from infra/demo/init/ to set up users and grants. Writes connection details to .ch-state.json.
  • Global teardown (tests/global-teardown.ts): stops the container.
  • Fixtures (tests/fixtures.ts): provides chConfig (connection details) and connectedPage (a Page with an active ClickHouse connection). Two connection methods:
    • connectViaLocalStorage — injects the connection profile directly into localStorage (fast, used by most tests)
    • connectViaUI — fills in the "Add Connection" form like a real user (used by connection form tests)

Browser projects

ProjectDeviceNotes
chromiumDesktop ChromeFull test suite
mobile-chromePixel 7Connected page tests + boot/responsiveness. Connection form and settings tests are skipped (nav overflow on narrow viewports).

Firefox and WebKit are available but commented out in playwright.config.ts. Enable them after installing browsers with npx playwright install firefox webkit.

Artifacts

  • Screenshots: captured on failure (test-results/)
  • Traces: captured on first retry (test-results/)
  • HTML report: npx playwright show-report (after a test run)
  • Video: captured on first retry

Adding new e2e tests

  1. For tests that need a ClickHouse connection, use the connectedPage fixture:
import { test, expect } from './fixtures';

test('my feature works', async ({ connectedPage: page }) => {
await page.goto('/#/my-page');
// connectedPage already verified data is flowing
await expect(page.getByText('something from ClickHouse')).toBeVisible();
});
  1. For UI-only tests (no ClickHouse needed), use the standard page fixture:
import { test, expect } from '@playwright/test';

test('UI renders correctly', async ({ page }) => {
await page.goto('/#/overview');
await expect(page.locator('header nav')).toBeVisible();
});
  1. Skip tests on mobile when they rely on desktop-only UI:
function skipOnMobile() {
if (test.info().project.name.includes('mobile')) test.skip();
}

Quick reference

just test                    # Run all tests (unit + frontend + integration)
just test-core # Unit tests only (packages/core)
just test-frontend # Frontend tests only
just test-core-integration # Integration tests only (requires Docker)
just test-data-utils # Data utils tests (requires Docker)
just e2e # E2E browser tests (requires Docker)
just e2e-ui # E2E with interactive Playwright UI
just e2e-headed # E2E with visible browser (slow motion)
just test-tag security # Run only tests tagged 'security'
just test-list-tags # List all available tags
just test-report # Open HTML report in browser