Skip to content

add a guide on unit testing job code #840

Description

@mtuchi

Context

openfn compile now writes compiled job expressions to disk, which means job code can finally be imported into a normal JavaScript test runner. Job expressions are not valid JS out of the box (top-level adaptor calls like get('/endpoint') break
the import), so until now there was no first-class way to unit test the helper functions users write inside their jobs.

There's an internal guide in kit covering the mechanics:
https://github.com/OpenFn/kit/blob/main/.claude/unit-testing-jobs.md

That guide is written for engineers who already know the compiler. We need a user-facing version on docs.openfn.org that is task-oriented: "here is how you test your job code", not "here is what --exports-only does".

Problem

Users writing non-trivial transformation logic (date formatting, SMS parsing, identifier mapping, code lookups) currently have no documented way to test it except by running the whole workflow with the CLI and eyeballing output.json. That's slow, doesn't isolate failures, and doesn't run in CI.

Proposed change

Add a new page: Writing unit tests for your jobs

Suggested location: alongside the job-writing guide, e.g. documentation/jobs/unit-testing-jobs, so it sits with the other "how to write good job code" content rather than buried in the CLI reference.

Cross-links needed from:

  • documentation/jobs/job-writing-guide — a short "your helpers can be unit tested" pointer
  • documentation/cli-usage — add compile to the common-usage examples with a link here
  • documentation/cli — mention testing in the "you can use the CLI to…" list

Content outline

  1. Why unit test job code — what is and isn't testable. Operations
    (fn, get, each) are not unit-testable; pure helper functions are. Set
    expectations up front.
  2. Write your helpers so they're testable — the most important section, and
    the one missing from the kit guide. In --exports-only mode only
    export const and export function declarations survive; a plain
    const formatDate = ... is dropped. So the guidance is: export any helper
    you want to test.
    Include a before/after snippet.
  3. Compile your workflowsopenfn compile --exports-only, what lands in
    dist/, the .mjs extension (and why that means you don't need
    "type": "module"), and -o / --clean / --workspace.
  4. Write a test — a full worked example, source → compiled → test, using
    Node's built-in test runner (node --test). Reuse the formatDate example
    from the kit guide or write a slightly richer one (e.g. parsing an SMS string
    into a structured record) so it demonstrates real value.
  5. The edit → test loop--watch alongside the test runner's watch mode.
  6. Wire it into your projectpackage.json scripts, .gitignore the
    output dir, and a short note on running this in CI (GitHub Actions snippet
    would be a nice-to-have, not blocking).
  7. Reference tablecompile flags: --exports-only, -o, -O,
    --watch, --clean, --workspace, plus the dirs.compiled key in
    openfn.yaml.
  8. Troubleshooting — see below; this is where most of the new writing is.

Gotchas that must be covered

These are the things that will generate support questions if we skip them:

  • "My helper isn't in the compiled file." It wasn't exported. Non-exported
    declarations are always dropped in strip mode.
  • "No file was written for my step." Steps whose compiled output is empty
    after stripping are skipped entirely, so the import fails with a
    module-not-found error rather than anything descriptive.
  • Adaptor imports are preserved in compiled output. A step that does
    import { dateFns } from '@openfn/language-dhis2' will still carry that import
    after compilation, so the adaptor package has to be installed locally for the
    test to run. This needs an explicit "install your adaptors as devDependencies"
    instruction, or a documented pattern for keeping testable helpers free of
    adaptor imports. This is the biggest open gap in the current guide.
  • export default is removed in strip mode — expected, since it only exists
    for the runtime.
  • Import paths in tests point at the compiled output (../dist/...), not the
    source. Worth stating plainly, and worth noting that the compiled dir should be
    gitignored so tests depend on a build step.
  • Full compilation (no --exports-only) writes every step and keeps operations
    in export default [...]. Mention it briefly so users know the flag matters,
    but don't make it the focus.

Acceptance criteria

  • New page published under documentation/ and added to the sidebar
  • Page includes a complete, copy-pasteable worked example (source → compiled → test → command to run it)
  • All CLI commands and flags in the page have been run against the current released @openfn/cli and produce the documented output
  • The adaptor-dependency gotcha is documented with a working solution, not just a warning
  • All eight gotchas above are covered somewhere on the page
  • Cross-links added from the job-writing guide and CLI usage pages
  • Minimum required @openfn/cli version stated on the page

Out of scope

  • Integration/end-to-end testing of whole workflows (running with fixture state,
    asserting on final state). Worth a follow-up page; keep this one focused on
    unit tests.
  • Mocking adaptor operations or HTTP calls.
  • Recommending a specific third-party test runner. Use node --test in examples
    and note that any runner works.

Open questions

  • Which @openfn/cli version first shipped compile with --exports-only? Needs
    confirming against the kit release notes / changelog before the page states a
    minimum version — I don't want to guess at that.
  • Does the compiled output dir get a .gitignore automatically (as .cli-cache
    does), or does the user need to add it? Affects the "wire it into your project"
    section.
  • Should compile also get a proper entry in the CLI reference docs as part of
    this issue, or as a separate one?
  • Is there an existing testing/quality section in the docs IA this should live
    under, rather than a standalone page?

Source material

Metadata

Metadata

Assignees

Type

No type

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions