-
Notifications
You must be signed in to change notification settings - Fork 3
Description
Context
We need to establish clear testing standards for ourselves and contributors.
Here are some important details about the state of our repo today:
- Legacy tests (from pre-open source): Mix of JS/TS, CJS/ESM, both unit and integration
- Migration Notes
- Migrated JS ESM based integration tests (https://github.com/HarperFast/harper/tree/main/integrationTests/apiTests)
- Still actively migrating old unit-tests (https://github.com/HarperFast/harper/tree/main/unitTests & More and better unit tests #15)
- Migration Notes
- Starting to write new tests (Improve Harper Application Management with lockfile #11)
- Codebase source is mix of JS/CommonJS and newer TS (compiled to CommonJS)
- Future goals for source code is ESM, stricter types, and Node.js type stripping during development (implies executing our import-syntax TS as ESM)
This inconsistency creates an unnecessary maintenance burden, confusion for all contributors, and challenges with reliability and scalability.
Integration Tests vs Unit Tests
First, let's clarify what we mean by these test types in the context of our application platform.
Integration Tests
Integration tests verify the full application workflow - how users actually interact with the platform:
- Testing CLI commands end-to-end
- Testing Operations API with full request/response cycles
- Testing complete application lifecycle
These tests invoke the application externally, not by importing source code directly. Harper must be running for these test to work (or they may test how the Harper application as a whole boots-up, reloads, shuts down, etc.)
Unit Tests
Unit tests verify the programmatic APIs that users import and use in their code:
- Global APIs used in
jsResourceapplication code - New plugin API
- Logger instance, Resource API, Server middleware, etc.
These tests import and call code directly, the same way users would in plugins or applications.
Why this distinction matters for our standards:
Integration tests don't depend on source code directly; thus, can freely use whatever language, module type, or test runner regardless of source code state.
Unit tests import source code directly; thus, need to consider (language/module) compatibility, and furthermore what we're actually testing (runtime behavior vs type-checked behavior)
What Should We Unit Test?
Our primary focus for unit tests should be the user-facing programmatic APIs (logger, database client, HTTP server, etc.) - the code that users import and interact with in their plugins and applications.
However, there are valid cases for unit testing internal logic:
- Complex algorithms with many edge cases
- Error conditions difficult to trigger through an integration test
- Frequently-changing core logic where fast feedback is valuable
- Internal modules heavily reused across the codebase
Solution: use your best judgement
If an internal module is simple and fully covered by integration tests, additional unit tests may not provide value. If it's complex or has hard-to-reach edge cases, unit tests are worthwhile. Integration tests should be preferred over unit tests, but more tests the merrier (as long as they aren't slow or flaky).
Proposal
Integration Tests
All integration tests should be TypeScript + ESM
- These tests invoke the application externally (don't import source code)
- Already working independently
- Aligns with our codebase direction (TypeScript + ESM migration)
- Allows us to start using Node.js Test Runner and Type Stripping today for Node.js v22+
- Node.js v20 doesn't support Type Stripping natively so we must figure out an appropriate solution; likely can use Tsx as a loader and it'll work fine.
We only have to update the existing apiTests/ to TS and all new tests can follow patterns being created in #11
Unit Tests (Two Options)
This is where it gets interesting. Unit tests need to import our source code, and we need to consider what we're actually testing.
Option 1: JavaScript + ESM
Pros:
✅ Easier to test runtime behavior without type constraints
✅ Can test "what happens when users ignore types" naturally (e.g., passing a number to a function expecting a string)
✅ Lower barrier for contributors unfamiliar with TypeScript
✅ Tests the lowest common denominator - actual runtime behavior
Cons:
❌ Miss TypeScript benefits (autocomplete, type checking, refactoring support)
❌ Doesn't align with codebase direction
❌ Less maintainable as codebase becomes more TypeScript-heavy
Option 2: TypeScript + ESM (with pragmatic runtime testing)
Pros:
✅ Aligns with codebase migration direction
✅ Better DX: autocomplete, type checking, easier refactoring
✅ Matches what we're building toward (Node.js Test Runner and Type Stripping)
✅ Can still test runtime behavior using @ts-expect-error, as any, or type escape hatches
✅ Consistency with integration test standard
Cons:
❌ Slightly more verbose for invalid input testing
❌ Requires contributors to use type escape hatches when testing runtime behavior
Example:
test('logger.info handles invalid input gracefully', () => {
// @ts-expect-error - testing runtime behavior with wrong type
assert.throws(() => logger.info(123)).toThrow('Expected string');
});My thoughts
I'm leaning toward TypeScript + ESM for all tests (both integration and unit) because of the consistency throughout all aspects of the repo. Yes we might have to use some TS hacks to validate runtime behavior, but I'm okay with that as we'll still benefit from all the other aspects of TS. The slight verbosity of @ts-expect-error is a small price to pay for the benefits of TypeScript tooling and consistency.
Next Steps
- Decide on the standard - get team consensus
- Document in CONTRIBUTING.md - clear guidance for contributors
- Create test templates/examples - show both happy path and runtime testing patterns
- Migrate opportunistically - no big rewrite needed, update tests as we touch them
- Maybe tracking issues?
- Update CI/tooling - ensure test runner supports the chosen standard