Testing
This project uses Vitest for all test suites. Tests ensure the correctness of codec implementations, parser wiring, schema alignment and important runtime behaviors that are difficult or impractical to fully express in TypeScript's type system.
Purpose
- Describe the test strategy used across the
packages/parsers
package. - Show how device examples (the spec-required examples file) are used as authoritative test fixtures.
- Explain how we test register lookups and why we use controlled typecasting in tests.
- Provide commands and quick tips for running targeted tests in the monorepo.
Note: Although benchmarks were considered during development, they are not part of the project's test requirements and are intentionally omitted from this guide.
Test tooling and scripts
We use Vitest as the test runner. The repository root
package.json
exposes the main scripts:pnpm test
: runs Vitest in watch mode (depending on your local environment).pnpm coverage
: runs Vitest once and collects coverage information.
If you want to run a single test file or pattern with Vitest from the repo root:
pnpm test -- <pattern-or-path>
Test types and organization
We organize tests into a few focused areas:
- Unit tests for shared utilities (for example
utils
helpers that convert numbers and bytes). - Unit tests for codec utilities and runtime validations (for example
checkChannelsValidity
,checkCodecsValidity
). - Device-level integration tests that exercise the full parser stack for a single device using the device
examples.json
fixtures and the generated Valibot schemas. - Register lookup and mapping tests that validate our runtime expectations for typecasts we apply in places where the static type system would be too complex or slow to model.
Examples in the repository show these patterns under packages/parsers/__tests__
and device-specific test files under packages/parsers/src/devices/<DEVICE>/*.test.ts
.
Device example testing (spec-required examples)
Each device folder that implements a parser includes an examples.json
file containing the canonical examples required by the device spec. We use these examples as integration tests.
The typical test flow for device examples is:
- Import the device
useParser()
factory from the device'sparser
module. - Import the
examples.json
file and filter examples by type (uplink, hexUplink, etc.). - Import the Valibot-generated schema factory for the device output (for example
createPEWUplinkOutputSchema
) and validate the decoded output withv.parse(...)
to ensure the implementation matches the schema.
This pattern ensures three guarantees in a single test:
- The codec/parser implementation decodes the provided example input to the expected output structure.
- The decoded output actually conforms to the runtime schema that will be published with the package (Valibot runtime validation).
- Any accidental divergence between examples, implementation and schema is caught early in CI.
A minimal device test example (repository contains a real example) follows this shape:
import * as v from 'valibot'
import examples from './examples.json' assert { type: 'json' }
import { useParser } from './parser'
import { createPEWUplinkOutputSchema } from './schema'
const { decodeUplink, decodeHexUplink } = useParser()
const outputSchema = createPEWUplinkOutputSchema()
examples.filter(e => e.type === 'uplink').forEach((ex) => {
const out = decodeUplink(ex.input as any)
expect(out).toEqual(ex.output)
expect(() => v.parse(outputSchema, out)).not.toThrow()
})
Register lookup tests and controlled typecasting
Some parts of the codebase (for example the register mapping and lookup machinery used by codecs) are intentionally implemented with light-weight, explicit typecasts in tests and factories. Modeling all possible register shapes precisely in TypeScript would add significant complexity and reduce developer productivity, while giving marginal type-safety benefits at runtime.
To keep the codebase maintainable and fast to change, we therefore:
- Keep run-time logic simple and pragmatic (often casting to
any
or to narrow shapes inside factories where the shape is known by construction). - Rely on thorough tests to ensure that the casts are correct. Tests exercise the register lookup code paths with representative inputs and assert the runtime outputs precisely.
Concretely:
- Register tables used by codecs are hard-coded in factories and tests will import these tables and assert the resolved register values and conversions.
- Tests validate boundary cases (missing registers, updated register values, type conversions, and error handling).
- When a cast is used in production code for ergonomics, a focused unit test exists that proves the cast matches the actual runtime structure produced by the device examples and schema.
This approach keeps the TypeScript types practical and avoids over-engineering the type system while preserving safety through deterministic tests.
Test authoring tips
- Prefer small, focused unit tests for utilities and codec internals. Keep device tests as end-to-end as practical using the
examples.json
files. - When adding a new device parser, add at least one
examples.json
(the spec-required file) and a device test that exercises uplink and hex uplink examples. - For complex register transformations, add unit tests that import the register map and exercise the lookup and conversion logic.
- Use
vi.fn()
to create spies/mocks when you need to assert that helper functions or codec hooks were invoked (see existing parser tests for examples).
Running tests in CI and locally
- Locally, run the parsers package tests with:
pnpm --filter ./packages/parsers test
- Generate coverage for local inspection:
pnpm --filter ./packages/parsers coverage
- Continuous integration should run
pnpm test
andpnpm coverage
at minimum for thepackages/parsers
package to ensure no regressions.
Coverage expectations and contribution policy
- Tests are the primary mechanism to guard correctness where types are intentionally relaxed.
- When introducing casts or simplifying typings, add tests that verify the runtime structure used by codecs and register lookups.