Test Automation with Deno

work in progress

As previously mentioned, I’ve cautiously come to appreciate Deno’s development tooling. This sometimes includes using its test runner, mostly because it’s both simple and readily available while adhering to common conventions.

Nevertheless, there are a few idiosyncratic details and conventions worth pointing out – if only to reduce friction and lower the barrier to pursuing test-driven development from the get-go. In fact, for our purposes here, I’ll recklessly expand the definition of “testing” to include static verification: formatting and linting for stylistic consistency as well as optional type checking – thus establishing some basic infrastructure for any JavaScript project, regardless of size or ambition.

Tasks

{
    "tasks": {
        "vet": "deno lint && deno fmt --check",
        "verify": "deno check --import-map ./deno.json ./src"
    },
    // …
}
deno.json

These two designations aren’t perfect, but were chosen to avoid overloading terms already reserved for Deno standard commands.

If we require custom TypeScript configuration, e.g. to avoid TypeScript syntax, we can pass that to deno check via --config ./tsconfig.json.

My shell history typically includes this combo:

$ deno task vet && deno task verify && deno test --parallel

Co-Located Tests

I’ve accepted the value of reduced distance by having test modules reside alongside the respective implementation (e.g. src/util.js and src/util.test.js).

Nevertheless, for overarching scenarios, a separate top-level test directory can be a useful addition.

Behavior-Driven Development (BDD)

While I’m not a fan of BDD nomenclature and grammar, Deno’s default approach lacks familiar hooks for setup and teardown – so I typically end up using BDD style anyway.

Import Maps

For consistency, we want to define standard-library URLs in one central place, ensuring we’re using the same version throughout. While we might create a proxy module which just selectively re-exports the respective functionality, that quickly becomes annoying and a bit of a maintenance burden.

Deno’s support for import maps makes this pretty simple though:

{
    "imports": {
        "$deno/": "https://deno.land/std@0.224.0/"
    },
    // …
}
deno.json
import { describe, it } from "$deno/testing/bdd.ts";
import {
    assertEquals as assertDeep,
    assertStrictEquals as assertSame,
} from "$deno/assert/mod.ts";

describe("calculator", () => {
    it("supports numbers", () => {
        assertSame(5 * 5, 25);
        assertDeep([3, 2, 1].toReversed(), [1, 2, 3]);
    });
});

deno test will pick this up automatically, substituting $deno/ within import statements accordingly. However, deno check currently requires explicitly adding --import-map ./deno.json and emits spurious warnings about supposedly invalid top-level keys within that file. 🤷

Note that editors and other tooling might not be aware of import maps within deno.json by default.