Skip to content

Testing Standards: Integration vs Unit Tests, TypeScript vs JavaScript, CJS vs ESM #22

@Ethan-Arrowood

Description

@Ethan-Arrowood

Context

We need to establish clear testing standards for ourselves and contributors.

Here are some important details about the state of our repo today:

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 jsResource application 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    questionFurther information is requestedtestsMostly focused on tests, testing infrastructure, etc.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions