Component Testing Guide for Reventless
Purpose
This guide provides a standardized approach for testing Reventless components. It's based on patterns discovered during DCB component testing and existing test suites (PluginBehaviorTest, MessageTest, ProjectionTest).
Use this guide when adding tests for any Reventless component:
- Core components (Aggregate, EventLog, ReadModel, Plugin, etc.)
- New components (DcbEventLog, CommandHandler, etc.)
- Adapter implementations (DynamoDB, S3, SQS, etc.)
Slice-level Given-When-Then tests — aggregate
Behavior, DCBStateChangeSlice, read-model projections, DCBStateViewSlice, automation / translation slices, and cross-patternMapping_GWT— are covered bydocs/guides/given-when-then.mdand run through thereventless-gwtCLI. This guide is for the component / integration layer that sits below them (mocks, adapters, callbacks, operations).
Quick Reference: Test Structure Template
packages/<package-name>/tests/<component-name>/
├── <Component>Fixtures.res # Test specs, mocks, fixtures
├── <Component>Test.res # Pure unit tests (sync)
├── <Component>OperationsTest.res # Runtime logic tests (async)
└── <Component>CallbackTest.res # Handler/callback tests (async)
Example (DCB components):
packages/reventless/tests/dcb/
├── DcbFixtures.res
├── DcbTagTest.res
├── DcbEventLogOperationsTest.res
└── DcbCommandHandlerTest.res
Component Architecture Pattern Recap
Reventless components follow a consistent structure (see packages/doc/docs/internals/component-structure-pattern.md):
| File | Purpose | Tests Needed |
|---|---|---|
Component.res | Type definitions, outputs | Schema validation, type safety |
Component_Builder.res | Factory using functors | Integration tests (wire-up) |
Component_Adapter.res | Provider-agnostic adapter interface | Adapter compliance tests |
Component_Operations.res | Runtime business logic | Unit tests (encode/decode, logic) |
Component_Callback.res | Runtime handlers | Handler behavior tests (async) |
Testing priority:
- Operations — Core business logic (highest value)
- Callbacks — Handler behavior with mocks
- Adapters — Interface compliance (can use in-memory mocks)
- Builder — Integration (lowest priority, often covered by usage tests)
Step 1: Create Test Fixtures (<Component>Fixtures.res)
Shared infrastructure for all component tests.
What to Include
1.1 Test Spec Modules
Create minimal spec modules satisfying the component's Spec module type:
// For a component with module type Spec = { let name: string; @schema type event; ... }
module TestEventLogSpec: EventLog.Spec = {
let name = "TestEventLog"
@schema
type event =
| ItemCreated({id: string, name: string})
| ItemUpdated({id: string, field: string, value: string})
}
Guidelines:
- Keep specs minimal — only what's needed to exercise the component
- Use realistic domain examples (items, users, orders) for clarity
- Include positive cases (normal variants) and negative cases (untagged/invalid) if relevant
1.2 Mock Adapters
Implement in-memory versions of adapter interfaces:
type mockStorage = {
operations: Component_Adapter.operations,
getState: unit => 'state, // Inspect internal state
setState: 'state => unit, // Pre-seed state
reset: unit => unit, // Clear state between tests
}
let makeMockStorage = (): mockStorage => {
let state = ref(initialState)
let read = async (~query) => {
// In-memory implementation
}
let write = async (data) => {
// In-memory implementation
}
{
operations: {read, write},
getState: () => state.contents,
setState: (s) => state := s,
reset: () => state := initialState,
}
}
Key patterns from DCB tests:
-
Factory function returning closures (not global refs):
let makeMockStorage = (): mockStorage => {
let data = ref([]) // Fresh state per test
let read = async () => data.contents
let write = async (item) => data := data.contents->Array.concat([item])
{operations: {read, write}, ...}
} -
Inspection functions for assertions:
type mockStorage = {
operations: Adapter.operations,
getEvents: unit => array<rawEvent>, // Inspect stored data
publishedMessages: ref<array<published>>, // Capture side effects
} -
Failure injection for error path testing:
type mockStorage = {
// ...
failNextWrites: ref<int>, // Counter: fail N operations then succeed
}
let write = async (data) => {
if failNextWrites.contents > 0 {
failNextWrites := failNextWrites.contents - 1
Error("injected failure")
} else {
// normal write
}
} -
Reset function for
beforeEach:let reset = () => {
data := []
publishedMessages := []
failNextWrites := 0
}
1.3 Test Data Fixtures
Common test values reused across test files:
let testMeta: Message.meta = {
service: "test",
time: "2024-01-01T00:00:00Z",
ip: "127.0.0.1",
user: "test-user",
msgId: "msg-1",
correlationId: "corr-1",
}
let sampleEvent1 = TestSpec.ItemCreated({id: "item-1", name: "First"})
let sampleEvent2 = TestSpec.ItemUpdated({id: "item-1", field: "name", value: "Updated"})
Step 2: Pure Unit Tests (<Component>Test.res)
Synchronous tests for pure functions (no async, no mocks).
What to Test
- Utility functions: Parsing, serialization, validation
- Type conversions: JSON encoding/decoding helpers
- Pure logic: Calculations, transformations, filtering
Example: Testing a utility module
open Jest
open Expect
describe("ComponentUtils:", () => {
describe("parseId", () => {
test("valid UUID returns Some", () => {
expect(ComponentUtils.parseId("550e8400-e29b-41d4-a716-446655440000"))
->toEqual(Some("550e8400-e29b-41d4-a716-446655440000"))
})
test("invalid string returns None", () => {
expect(ComponentUtils.parseId("not-a-uuid"))->toEqual(None)
})
})
describe("extractTags", () => {
test("tagged field extracts correctly", () => {
let event = TestSpec.ItemCreated({id: "item-1", name: "Test"})
expect(ComponentUtils.extractTags(TestSpec.eventSchema, event))
->toEqual([{key: "id", value: "item-1"}])
})
test("untagged variant returns empty array", () => {
let event = UntaggedSpec.PlainEvent({data: "test"})
expect(ComponentUtils.extractTags(UntaggedSpec.eventSchema, event))
->toEqual([])
})
})
})
Patterns
-
Given/When/Then style test names:
- ✅
"valid UUID returns Some" - ✅
"tagged field extracts correctly" - ❌
"test parseId"(too vague)
- ✅
-
Organize by function with nested
describeblocks -
Test edge cases:
- Empty inputs
- Boundary values
- Invalid/malformed data
Step 3: Operations Tests (<Component>OperationsTest.res)
Async tests for runtime business logic (typically Component_Operations.res).
Setup Pattern
open Jest
open Expect
let mock = ComponentFixtures.makeMockStorage()
module TestOps: Component_Operations.Ops with module Spec = ComponentFixtures.TestSpec = {
module Spec = ComponentFixtures.TestSpec
let storage = mock.operations
// ... other dependencies
}
module Ops = Component_Operations.Make(ComponentFixtures.TestSpec, TestOps)
let _ = beforeEach(() => mock.reset())
Key points:
- Use
with module Spec = ...to refine the module type - Call
mock.reset()inbeforeEachto isolate tests - Instantiate the functor once (not per test)
What to Test
3.1 Round-trip Tests (Encode/Decode)
Test that domain values survive serialization:
describe("round-trip (write then read)", () => {
testPromise("ItemCreated preserves through round-trip", async () => {
let event = ComponentFixtures.TestSpec.ItemCreated({id: "item-1", name: "Test"})
let _ = await Ops.write(event)
let result = await Ops.read("item-1")
expect(result)->toEqual(Some(event))
})
testPromise("multiple items all read back", async () => {
let events = [event1, event2, event3]
let _ = await Ops.writeAll(events)
let result = await Ops.readAll()
expect(result)->toEqual(events)
})
})
Watch out for (learned from DCB tests):
- Payload-less variants may not round-trip correctly (depending on serialization strategy)
- Tagged schemas need
@s.matcheson the type expression, not field name:// ✅ Correct:
{id: @s.matches(DcbTag.string) string}
// ❌ Wrong (silently ignored):
{@s.matches(DcbTag.string) id: string}
3.2 Business Logic Tests
Test the core operations:
describe("append", () => {
testPromise("stores event and publishes to topic", async () => {
let event = TestSpec.ItemCreated({id: "item-1", name: "Test"})
let result = await Ops.append([event])
expect((
Result.isOk(result),
mock.getEvents()->Array.length,
mock.publishedMessages.contents->Array.length,
))->toEqual((true, 1, 1))
})
testPromise("error from storage does not publish", async () => {
mock.failNextWrites := 1
let result = await Ops.append([event])
expect((
Result.isError(result),
mock.publishedMessages.contents->Array.length,
))->toEqual((true, 0))
})
})
describe("read", () => {
testPromise("returns empty for non-existent ID", async () => {
let result = await Ops.read("non-existent")
expect(result)->toEqual(None)
})
testPromise("filters by query parameter", async () => {
let _ = await Ops.append([event1, event2])
let result = await Ops.read(~query={status: "active"})
expect(result->Array.length)->toEqual(1)
})
})
3.3 Error Handling
Test failure paths:
describe("error handling", () => {
testPromise("storage failure returns Error", async () => {
mock.failNextWrites := 1
let result = await Ops.write(data)
expect(Result.isError(result))->toBe(true)
})
testPromise("invalid data returns Error", async () => {
let result = await Ops.write(invalidData)
expect(result)->toEqual(Error("validation failed"))
})
})
Step 4: Callback/Handler Tests (<Component>CallbackTest.res)
Async tests for event handlers, command handlers, or callback logic.
Setup Pattern
For components with multiple dependencies (e.g., CommandHandler needs DcbEventLog):
let mock = ComponentFixtures.makeMockStorage()
// Instantiate dependency components
module DependencyOps = Dependency_Operations.Make(...)
// Stub module for unused builder functions
module StubDependency = {
module Spec = DependencySpec
type operations = {read: ..., write: ...}
type component = Dependency.component<operations>
// Never called in tests — satisfies module type only
let make = (~name as _, ~opts as _=?): component => Obj.magic(0)
}
// Instantiate component under test
module TestOps = {
module Spec = ComponentFixtures.TestSpec
module Dependency = StubDependency
let dependency: StubDependency.operations = {
read: DependencyOps.read,
write: DependencyOps.write,
}
}
module Handler = Component_Callback.Make(ComponentFixtures.TestSpec, TestOps)
Pattern: Use Obj.magic(0) for builder functions (make) that aren't called in tests. Document clearly: // Never called — satisfies module type only.
What to Test
4.1 Happy Path
Test normal execution flow:
describe("handleCommand - happy path", () => {
testPromise("CreateItem succeeds and emits event", async () => {
let command = TestSpec.CreateItem({id: "item-1", name: "Test"})
let result = await Handler.handleCommand(command)
expect((
Result.isOk(result),
mock.getEvents()->Array.length,
))->toEqual((true, 1))
})
testPromise("emitted event has correct data", async () => {
let _ = await Handler.handleCommand(command)
let event = mock.getEvents()->Array.getUnsafe(0)
expect(event.eventType)->toEqual("ItemCreated")
})
})
4.2 Error Cases
Test failure paths:
describe("handleCommand - errors", () => {
testPromise("invalid state returns Error", async () => {
// Pre-seed conflicting state
mock.setState({exists: true})
let command = TestSpec.CreateItem({id: "item-1", name: "Test"})
let result = await Handler.handleCommand(command)
expect(result)->toEqual(Error("ItemAlreadyExists"))
})
testPromise("error does not emit events", async () => {
mock.setState({exists: true})
let _ = await Handler.handleCommand(createCommand)
expect(mock.publishedMessages.contents->Array.length)->toEqual(0)
})
})
4.3 Retry/Concurrency Logic
Test retry mechanisms (if applicable):
describe("handleCommand - retry on conflict", () => {
testPromise("retries and succeeds after 1 failure", async () => {
mock.failNextWrites := 1
let result = await Handler.handleCommand(command)
expect((
Result.isOk(result),
mock.getEvents()->Array.length,
))->toEqual((true, 1))
})
testPromise("returns Error after retries exhausted", async () => {
mock.failNextWrites := 4 // Exhaust all retries
let result = await Handler.handleCommand(command)
expect((
Result.isError(result),
mock.getEvents()->Array.length,
))->toEqual((true, 0))
})
})
Counter-based failure injection (from DCB tests):
- Use
failNextOperations: ref<int>counter in mock - Decrement on each call, fail if > 0
- Enables deterministic retry testing (no race conditions)
4.4 Batch Handling
If the handler processes batches:
describe("handleCommands - batch", () => {
testPromise("multiple successful commands", async () => {
let results = await Handler.handleCommands([command1, command2])
expect(results)->toEqual([Ok("ref-1"), Ok("ref-2")])
})
testPromise("mixed success and failure", async () => {
// Pre-seed state to cause second command to fail
let results = await Handler.handleCommands([validCommand, invalidCommand])
expect(results)->toEqual([Ok("ref-1"), Error("ref-2")])
})
testPromise("empty batch returns empty array", async () => {
let results = await Handler.handleCommands([])
expect(results)->toEqual([])
})
})
ReScript Testing Patterns & Gotchas
Pattern 1: Type Annotations for Record Field Resolution
Problem: ReScript loses track of record types in pattern matching.
// ❌ Fields not resolved:
switch optionalRecord {
| Some(rec) => rec.field // Error: field not found
| None => defaultValue
}
// ✅ Add type annotation:
switch optionalRecord {
| Some(rec: RecordType) => rec.field // Works
| None => defaultValue
}
Pattern 2: Abstract Types Need Constructors
Problem: Abstract types (defined in .resi files) don't unify with primitives.
// ❌ Type error:
id: "string-value" // Expected: Id.t, got: string
// ✅ Use constructor:
id: Id.makeFromString("string-value")
Common in Reventless:
Reventless.Id.String.t— useId.String.makeFromString()Reventless.Id.Int.t— useId.Int.makeFromInt()
Pattern 3: Return Type Annotations for Abstract Types
Problem: Type inference fails for functions returning values with abstract types.
// ❌ Infers wrong type (string instead of Id.t):
let makeCommand = (ref, cmd) => {
command: {
id: Id.makeFromString("cmd-" ++ ref),
command: cmd,
},
reference: ref,
}
// ✅ Explicit return type:
let makeCommand = (ref, cmd): CommandTopic.topicItem<
Message.command'<Id.String.t, TestSpec.command>
> => {
command: {
id: Id.makeFromString("cmd-" ++ ref),
command: cmd,
},
reference: ref,
}
Pattern 4: Module Type Refinement with with
Problem: Functor expects a module with a specific Spec, but the constraint is abstract.
// ❌ Type mismatch (Spec is abstract):
module TestOps: Component_Operations.Ops = {
module Spec = TestSpec
// ...
}
// ✅ Refine with `with module Spec = ...`:
module TestOps: Component_Operations.Ops with module Spec = TestSpec = {
module Spec = TestSpec
// ...
}
Pattern 5: @s.matches on Type Expressions
Problem: Placing @s.matches(schema) on the field name is silently ignored.
// ❌ Silently ignored by sury-ppx:
{@s.matches(DcbTag.string) fieldName: string}
// ✅ On type expression (after colon):
{fieldName: @s.matches(DcbTag.string) string}
Verification: Check compiled .res.mjs output:
- ✅ Correct:
s.m(DcbTag$Reventless.string) - ❌ Wrong:
s.m(S.string)
Pattern 6: Payload-less Variants May Not Round-trip
Problem: Variants without payloads (e.g., | SimpleEvent) may not survive encode/decode depending on serialization strategy.
Example from DCB tests:
- sury serializes
SimpleEventto JSON string"SimpleEvent" Message.splitMessageexpects JSON object withTAGfield- Result: crashes with
"Unknown"event type
Solution: If your component uses Message.splitMessage/combineMessage, avoid payload-less variants in production code. Test them separately for extractTags (works correctly), but exclude from round-trip tests.
Pattern 7: Obj.magic for Stubbing Unused Functions
Use case: Module type requires a function that won't be called in tests.
module StubComponent = {
module Spec = TestSpec
type operations = {...}
type component = Component.component<operations>
// Never called — satisfies module type only
let make = (~name as _, ~opts as _=?): component => Obj.magic(0)
}
Safety:
- Document clearly:
// Never called — satisfies module type only - If accidentally called, fails immediately (doesn't corrupt state)
- Only use for builder functions, not for operations used in tests
Test Organization Guidelines
File Naming
<Component>Fixtures.res # Test specs, mocks, test data
<Component>Test.res # Pure unit tests (sync)
<Component>OperationsTest.res # Runtime logic tests (async)
<Component>CallbackTest.res # Handler tests (async)
<Component>AdapterTest.res # Adapter compliance tests (if needed)
Directory Structure
packages/<package-name>/tests/
├── <component1>/
│ ├── <Component1>Fixtures.res
│ ├── <Component1>Test.res
│ └── <Component1>OperationsTest.res
├── <component2>/
│ ├── <Component2>Fixtures.res
│ └── <Component2>Test.res
└── integration/
└── <Scenario>IntegrationTest.res
Example (existing):
packages/reventless/tests/
├── plugin/
│ ├── PluginFixtures.res
│ ├── PluginBehaviorTest.res
│ └── PluginProjectionTest.res
├── dcb/
│ ├── DcbFixtures.res
│ ├── DcbTagTest.res
│ ├── DcbEventLogOperationsTest.res
│ └── DcbCommandHandlerTest.res
├── MessageTest.res
└── TestFixtures.res
Test Naming Conventions
Describe Blocks
describe("<ComponentName>:", () => { // Top level: component name
describe("<functionName>", () => { // Second level: function/operation
describe("- <scenario>", () => { // Third level: scenario category
testPromise("<specific case>", ...) // Test case: specific behavior
})
})
})
Example:
describe("CommandHandler_Callback:", () => {
describe("handleCommand", () => {
describe("- happy path", () => {
testPromise("CreateItem succeeds and emits event", async () => {
// ...
})
})
describe("- error cases", () => {
testPromise("invalid state returns Error", async () => {
// ...
})
})
})
})
Test Case Names
Use Given/When/Then style:
// ✅ Good (describes behavior):
"ItemCreated preserves through round-trip"
"error from storage does not publish"
"returns empty array for non-existent ID"
"retries and succeeds after 1 failure"
// ❌ Bad (too vague):
"test append"
"check read"
"handle error"
Mock Storage Design Patterns
Pattern A: In-Memory Array Storage
For event logs, message queues, simple stores:
let makeMockStorage = (): mockStorage => {
let items = ref([])
let position = ref(0)
let append = async (newItems) => {
items := items.contents->Array.concat(newItems)
position := position.contents + newItems->Array.length
Ok(position.contents->Int.toString)
}
let read = async (~after=?) => {
let filtered = switch after {
| Some(pos) => items.contents->Array.filter(item => item.position > pos)
| None => items.contents
}
{items: filtered}
}
{
operations: {append, read},
getItems: () => items.contents,
reset: () => {
items := []
position := 0
},
}
}
Pattern B: In-Memory Dict Storage
For key-value stores, databases:
let makeMockStorage = (): mockStorage => {
let store = ref(Dict.make())
let get = async (key) => {
store.contents->Dict.get(key)
}
let set = async (key, value) => {
store.contents->Dict.set(key, value)
Ok()
}
{
operations: {get, set},
getAll: () => store.contents,
reset: () => store := Dict.make(),
}
}
Pattern C: Side Effect Capture
For publishers, notifiers, external service calls:
let makeMockPublisher = (): mockPublisher => {
let published = ref([])
let publish = async (service, meta, json) => {
published := published.contents->Array.concat([{service, meta, json}])
}
{
publishJson: publish,
getPublished: () => published.contents,
reset: () => published := [],
}
}
Pattern D: Failure Injection
For retry logic, error handling:
let makeMockStorage = (): mockStorage => {
let data = ref([])
let failNextAppends = ref(0)
let append = async (items) => {
if failNextAppends.contents > 0 {
failNextAppends := failNextAppends.contents - 1
Error("injected failure")
} else {
data := data.contents->Array.concat(items)
Ok(data.contents->Array.length->Int.toString)
}
}
{
operations: {append},
failNextAppends: failNextAppends, // Test sets this
reset: () => {
data := []
failNextAppends := 0
},
}
}
Usage:
mock.failNextAppends := 1 // Next call fails, then succeeds
mock.failNextAppends := 4 // Exhaust retries (if max 3 retries)
Verification Checklist
Before submitting component tests:
Build & Test
-
npm run build— All modules compile -
npm test— All tests pass (new + existing) -
npx jest tests/<component>/— Isolated component tests pass
Coverage
- Pure functions tested (utils, conversions)
- Round-trip tested (encode → decode → matches original)
- Business logic tested (core operations, edge cases)
- Error paths tested (invalid inputs, storage failures)
- Retry logic tested (if applicable, using counter-based injection)
- Side effects captured and verified (publishes, notifications)
Code Quality
- No
Obj.magicexcept for unused builder functions (documented) - Mock storage uses factory functions (not global refs)
-
beforeEach(() => mock.reset())isolates tests - Type annotations where needed (record fields, abstract types)
- Test names follow Given/When/Then style
Documentation
- Fixtures file has comments explaining test specs
- Complex mocks have comments explaining behavior
- Findings/gotchas documented (if discovered new patterns)
Common Test Scenarios by Component Type
Event Log Components
Test:
- Round-trip: event → JSON → event
- Replay: read events for aggregate ID
- Append: optimistic concurrency (sequence numbers)
- Position tracking:
afterparameter filters correctly
Command Handler Components
Test:
- Decide: produces correct events from commands
- Reduce: builds decision model from events
- Retry: on conditional append conflict
- Batch: multiple commands, mixed success/failure
Read Model Components
Test:
- Projection: events → queryable state
- Query: filters, pagination, sorting
- Rebuild: from event stream
- Consistency: eventual consistency guarantees
Topic Components (Command/Event Topics)
Test:
- Publish: message enqueued
- Subscribe: message delivered to handler
- Ordering: FIFO guarantees (if applicable)
- Batching: multiple messages processed together
Adapter Components
Test:
- Interface compliance: satisfies adapter module type
- Error handling: network failures, timeouts
- Resource creation: Pulumi resources generated correctly
- Runtime behavior: actual storage operations work
Integration Testing (Optional)
For complex multi-component scenarios:
File: tests/integration/<Scenario>IntegrationTest.res
// Example: Full command flow
describe("Command flow integration:", () => {
testPromise("command → events → read model update", async () => {
// Setup: wire up Aggregate + EventLog + ReadModel
let eventLog = ...
let aggregate = ...
let readModel = ...
// Execute: send command
let _ = await aggregate.executeCommand(createCommand)
// Verify: event stored
let events = await eventLog.replay(aggregateId)
expect(events->Array.length)->toEqual(1)
// Verify: read model updated
let state = await readModel.query({id: aggregateId})
expect(state)->toEqual(Some(expectedState))
})
})
When to write integration tests:
- Multi-component workflows (command → events → projections)
- Adapter interoperability (DynamoDB + SQS + SNS)
- End-to-end scenarios (API → domain → storage → query)
Keep integration tests separate from unit tests (slower, harder to debug).
Examples from Existing Codebase
Pure Unit Tests
MessageTest.res— TestssplitMessage,combineMessage,generateMeta- Synchronous, no mocks
- Covers JSON encoding/decoding edge cases
Operations Tests
DcbEventLogOperationsTest.res— Tests encode/decode, append, read- Async with mock storage
- Round-trip tests for event preservation
- Side effect verification (publishJson calls)
Callback/Handler Tests
DcbCommandHandlerTest.res— Tests reduce/decide loop, retry logic- Async with multiple mocks (storage + publisher)
- Retry testing with counter-based failure injection
- Batch processing with mixed results
Behavior Tests
PluginBehaviorTest.res— Tests Aggregate behavior (init/apply/execute)- Uses
BehaviorTesthelper module - Given/When/Then test structure
- Focuses on domain logic, not infrastructure
- Uses
Projection Tests
PluginProjectionTest.res— Tests ReadModel projection- Uses
ProjectionTesthelper module - Event stream → state transformation
- Query verification
- Uses
Quick Start Checklist
To add tests for a new component:
- Create
tests/<component>/directory - Create
<Component>Fixtures.res:- Test spec module(s)
- Mock adapter(s) with factory function
- Test data fixtures
- Create
<Component>Test.res(if pure functions exist):- Test utils, conversions, pure logic
- Create
<Component>OperationsTest.res:- Round-trip tests
- Business logic tests
- Error handling tests
- Create
<Component>CallbackTest.res(if handlers exist):- Happy path tests
- Error case tests
- Retry/batch tests (if applicable)
- Run
npm run build && npm test - Document any new patterns/gotchas discovered
References
- Component Structure:
packages/doc/docs/internals/component-structure-pattern.md - Existing Tests:
packages/reventless/tests/— MessageTest, PluginBehaviorTest, ProjectionTest - Jest Bindings:
node_modules/@glennsl/rescript-jest/src/jest.resi - Sury Docs:
node_modules/sury-ppx/README.md
Maintenance
This guide should be updated when:
- New testing patterns are discovered
- New component types are added
- ReScript/sury-ppx version changes affect testing
- Better mock patterns are identified
Last updated: Based on DCB component testing (2024)