Skip to content

Commit e3fe71a

Browse files
committed
feat: add testing pattern guides (contract, scenario, progression)
Adds 3 testing guides to databricks-apps/references/appkit/: - contract-testing.md: PACT-style contract tests for cross-boundary data validation (frontend↔server, server↔lakebase, job↔job) - scenario-testing.md: Playwright e2e tests parameterized by JSON cases using the pw-evals pattern (public/private split) - testing-patterns.md: 5-level testing progression from smoke tests (mandatory) through unit, contract, scenario, to eval pipeline Updates SKILL.md Required Reading table with all three.
1 parent 67d31dd commit e3fe71a

File tree

5 files changed

+678
-0
lines changed

5 files changed

+678
-0
lines changed

manifest.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@
9696
"references/write-spark-declarative-pipelines.md"
9797
],
9898
"base_revision": "5c4b4fb0a82a"
99+
},
100+
"databricks-proto-first": {
101+
"version": "0.1.0",
102+
"description": "Proto-first schema design for Databricks apps",
103+
"experimental": false,
104+
"updated_at": "2026-03-31T11:39:30Z",
105+
"files": [
106+
"SKILL.md",
107+
"references/plugin-contracts.md"
108+
]
99109
}
100110
}
101111
}

skills/databricks-apps/SKILL.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ Build apps that deploy to Databricks Apps platform.
2424
| Adding API endpoints | [tRPC Guide](references/appkit/trpc.md) |
2525
| Using Lakebase (OLTP database) | [Lakebase Guide](references/appkit/lakebase.md) |
2626
| Typed data contracts (proto-first design) | [Proto-First Guide](references/appkit/proto-first.md) and [Plugin Contracts](references/appkit/proto-contracts.md) |
27+
| Testing progression | [Testing Patterns](references/appkit/testing-patterns.md) |
28+
| Contract testing | [Contract Testing](references/appkit/contract-testing.md) |
29+
| Scenario/E2E testing | [Scenario Testing](references/appkit/scenario-testing.md) |
2730
| Platform rules (permissions, deployment, limits) | [Platform Guide](references/platform-guide.md) — READ for ALL apps including AppKit |
2831
| Non-AppKit app (Streamlit, FastAPI, Flask, Gradio, Next.js, etc.) | [Other Frameworks](references/other-frameworks.md) |
2932

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# Contract Testing
2+
3+
PACT-style contract tests for AppKit apps. Consumer defines expectations, provider verifies. Use when multiple modules produce or consume the same data shapes.
4+
5+
**When to use:** Multi-module apps, apps with jobs that produce/consume data, or any app where two or more modules share a data boundary. Skip for single-module prototypes.
6+
7+
**Not mandatory.** Add contract tests when the app has multiple producers/consumers of the same data, or when a boundary change has caused a runtime bug.
8+
9+
## What Contract Tests Are
10+
11+
Contract tests verify that a producer's output matches what the consumer expects. They are NOT integration tests -- they run without network calls, databases, or live services.
12+
13+
The pattern (PACT-style):
14+
1. **Consumer** defines a contract: "I expect this shape, with these constraints."
15+
2. **Provider** is verified against that contract: "Does my output satisfy every consumer?"
16+
3. If either side changes, the contract test fails at build time -- not in production.
17+
18+
```
19+
Consumer (Dashboard) Provider (Eval API)
20+
| |
21+
+-- expects scores 0..1 |
22+
+-- expects run_id as string |
23+
+-- expects status enum |
24+
| |
25+
+---- contract.test.ts -----------+
26+
|
27+
vitest runs
28+
at build time
29+
```
30+
31+
## Contract Boundaries in AppKit Apps
32+
33+
Each module boundary is a potential contract surface:
34+
35+
| Boundary | Producer | Consumer | What to test |
36+
|----------|----------|----------|-------------|
37+
| frontend <-> server | tRPC router | React components | Response shapes, error codes, field presence |
38+
| server <-> lakebase | Lakebase migrations/queries | tRPC procedures | Row shapes, column types, NULL handling |
39+
| server <-> files | Files plugin | tRPC procedures | Volume paths, content types, metadata keys |
40+
| job <-> job | Upstream job task | Downstream job task | Task output shapes, status codes, payload encoding |
41+
42+
## How to Write Contract Tests with Vitest
43+
44+
Contract tests live alongside unit tests and run with `vitest`.
45+
46+
### Basic Example
47+
48+
```ts
49+
import { describe, it, expect } from "vitest";
50+
51+
// Simulated provider response -- in practice, import the type
52+
// and construct a minimal valid instance.
53+
const result = {
54+
run_id: "run-abc-123",
55+
appeval100: 0.87,
56+
status: "COMPLETED",
57+
metrics: { accuracy: 0.92, latency_ms: 340 },
58+
};
59+
60+
describe("Dashboard expects Eval API", () => {
61+
it("returns a valid run_id", () => {
62+
expect(typeof result.run_id).toBe("string");
63+
expect(result.run_id.length).toBeGreaterThan(0);
64+
});
65+
66+
it("returns results with scores between 0 and 1", () => {
67+
expect(result.appeval100).toBeGreaterThanOrEqual(0);
68+
expect(result.appeval100).toBeLessThanOrEqual(1);
69+
});
70+
71+
it("returns a known status enum value", () => {
72+
expect(["PENDING", "RUNNING", "COMPLETED", "FAILED"]).toContain(
73+
result.status
74+
);
75+
});
76+
77+
it("includes metrics as a record of numbers", () => {
78+
for (const [key, value] of Object.entries(result.metrics)) {
79+
expect(typeof key).toBe("string");
80+
expect(typeof value).toBe("number");
81+
}
82+
});
83+
});
84+
```
85+
86+
### Testing Lakebase Row Shapes
87+
88+
```ts
89+
import { describe, it, expect } from "vitest";
90+
import type { RunRecord } from "../proto/gen/app/v1/database";
91+
92+
// Minimal valid row -- mirrors what Lakebase would return.
93+
const row: RunRecord = {
94+
run_id: "run-001",
95+
app_name: "my-app",
96+
status: "RUN_STATUS_PENDING",
97+
started_at: new Date().toISOString(),
98+
completed_at: "",
99+
error_message: "",
100+
config_json: "{}",
101+
};
102+
103+
describe("API module expects RunRecord from Lakebase", () => {
104+
it("has required fields", () => {
105+
expect(row.run_id).toBeTruthy();
106+
expect(row.app_name).toBeTruthy();
107+
});
108+
109+
it("status is a valid enum string", () => {
110+
expect(row.status).toMatch(/^RUN_STATUS_/);
111+
});
112+
113+
it("config_json is valid JSON", () => {
114+
expect(() => JSON.parse(row.config_json)).not.toThrow();
115+
});
116+
});
117+
```
118+
119+
### Testing Job Task Outputs
120+
121+
```ts
122+
import { describe, it, expect } from "vitest";
123+
import type { JobTaskOutput } from "../proto/gen/app/v1/compute";
124+
125+
const output: JobTaskOutput = {
126+
task_id: "task-001",
127+
run_id: "run-001",
128+
success: true,
129+
error: "",
130+
output_payload: new Uint8Array([]),
131+
duration_ms: 1200,
132+
metrics: { rows_processed: "5000" },
133+
};
134+
135+
describe("API module expects JobTaskOutput", () => {
136+
it("has matching run_id and task_id", () => {
137+
expect(output.run_id).toBeTruthy();
138+
expect(output.task_id).toBeTruthy();
139+
});
140+
141+
it("duration_ms is non-negative", () => {
142+
expect(output.duration_ms).toBeGreaterThanOrEqual(0);
143+
});
144+
145+
it("on success, error is empty", () => {
146+
if (output.success) {
147+
expect(output.error).toBe("");
148+
}
149+
});
150+
});
151+
```
152+
153+
## Proto-First Contract Derivation
154+
155+
The recommended workflow ties contract tests directly to proto definitions:
156+
157+
```
158+
1. Write the contract test -> "Dashboard expects scores 0..1"
159+
2. Derive the proto message -> message EvalResult { double score = 1; }
160+
3. Generate TypeScript types -> buf generate proto/
161+
4. Implement provider -> tRPC route returns EvalResult
162+
5. Contract test passes -> Consumer expectation met
163+
```
164+
165+
This inverts the usual flow. Instead of writing the proto first and hoping consumers are satisfied, you start from what the consumer needs and work backward to the schema. The proto becomes the verified bridge.
166+
167+
## Running Contract Tests
168+
169+
Contract tests run with the rest of the vitest suite:
170+
171+
```bash
172+
# Run all tests including contracts
173+
npx vitest run
174+
175+
# Run only contract tests (by convention, name files *.contract.test.ts)
176+
npx vitest run --reporter=verbose "contract"
177+
```
178+
179+
## File Naming Convention
180+
181+
```
182+
tests/
183+
contracts/
184+
dashboard-eval-api.contract.test.ts
185+
api-lakebase-runs.contract.test.ts
186+
job-upstream-downstream.contract.test.ts
187+
```
188+
189+
Name each file `<consumer>-<provider>.contract.test.ts` so the boundary is obvious at a glance.
190+
191+
## Common Traps
192+
193+
| Trap | Why it fails | Fix |
194+
|------|-------------|-----|
195+
| Testing implementation, not shape | Test breaks on refactor even though contract holds | Assert on shape and constraints, not internal logic |
196+
| No contract for job boundaries | Job output changes silently, downstream breaks | Add contract test for every job->job and job->api boundary |
197+
| Duplicating validation logic | Contract and runtime validation diverge | Derive both from the proto; contract test checks the shape, runtime uses generated validators |
198+
| Testing only happy path | Missing fields or null values slip through | Add cases for empty strings, zero values, missing optional fields |

0 commit comments

Comments
 (0)