Skip to content
Back to skills

Testing Unit

ASecurity

Category-A unit tests for OwlMeans Common packages — no mocks, real sibling-package imports, services/components/helpers focus. Auto-invoked when writing tests in non-auth, non-integration, non-UI packages.

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 22, 2026
developmentgotestingapibackend

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 6, 2026

npx -y skills add owlmeans/common --skill testing-unit --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Testing Unit?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Testing Unit
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/owlmeans-testing-unit/badge)](https://www.skillsdirectory.com/skills/owlmeans-testing-unit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: testing-unit
description: Category-A unit tests for OwlMeans Common packages — no mocks, real sibling-package imports, services/components/helpers focus. Auto-invoked when writing tests in non-auth, non-integration, non-UI packages.
---

# Unit Tests — Category A (no mocks)

**Install:** `"@owlmeans/test": "^0.1.18-rc.35"` in `devDependencies`

Apply this skill when adding tests to packages in category A (see `testing-overview`). The list includes core abstractions (`context`, `config`, `error`, `entrypoint`, `route`, `resource`, …) and platform-agnostic services (`api`, `state`, `flow`, `i18n`, `client-flow`, `client-socket`, `client-job`, `server-route`, `server-context`, `web-db`, …).

`@owlmeans/test` is also the base of the other three harness packages, so its exports below are
available in every category.

## Helpers from `@owlmeans/test`

| Helper | Purpose |
|---|---|
| `loadEnv({ force?, file? })` | Read a `.env` into `process.env`, once per process. Never overwrites a variable already set to a non-empty value; a missing file is not an error. The root is found by walking up for a `bun.lock`, or for a `package.json` sitting beside a directory named `packages` — a workspace shaped any other way must pass `file` explicitly. |
| `hasEnv(key)` | `loadEnv()`, then true when the variable is set and non-empty. |
| `requireEnv(keys)` | `EnvGate` — `{ ok: true }` when every key is populated, otherwise `{ skip: true, reason }` naming the missing ones. |
| `makeGates(spec)` | Frozen `{ name: EnvGate }` built from `{ name: [envKey, …] }`. One call in `tests/context.ts` is the whole suite's availability map. |
| `isSkip(gate)` | Type guard narrowing an `EnvGate` to the skip branch, so `gate.reason` is readable. |
| `loadFixture<T>(relPath)` | Parse a JSON fixture from the package's own `tests/` directory — `bun test` sets `process.cwd()` to the package root, so the path is `tests/`-relative. |
| Types: `EnvGate`, `EnvOk`, `EnvSkip`, `GateSpec`, `Gates<S>` | The gate shapes, for typing a suite's exported map. |

### Gating a spec

A category-A package needs nothing external, so most specs never gate. When one does — a live
provider, an optional local binary — declare every gate in `tests/context.ts` and let each spec
choose its own `test` at module scope:

```ts
// tests/context.ts
import { makeGates } from '@owlmeans/test'

export const gates = makeGates({
  openrouter: ['OPENROUTER_SECRET'],
  anthropic: ['ANTHROPIC_SECRET'],
})
```

```ts
// tests/<area>.spec.ts
import { test } from 'bun:test'
import { isSkip } from '@owlmeans/test'
import { gates } from './context.js'

const gate = gates.openrouter
const it = isSkip(gate) ? test.skip : test
// `isSkip(gate) ? gate.reason : ''` names the missing variables in the skip title.
```

Bun decides `test` vs `test.skip` **synchronously**, so the decision has to be a value already in
hand — never an `await` inside the suite. An empty variable is a printed skip, never a failure.

## Layout

```
<your-package>/
├── src/...
├── tests/
│   ├── context.ts          # one real context, shared across specs
│   ├── <area>.spec.ts      # *.spec.ts only
│   └── fixtures/           # JSON fixtures, read with loadFixture('fixtures/<name>.json')
```

## `tests/context.ts` — single source of truth

Build a real context exactly the way the package's `SKILL.md` documents downstream apps building it. No mocks. Sibling packages are imported normally.

```ts
import { AppType, makeBasicContext } from '@owlmeans/context'
import type { BasicConfig, BasicContext } from '@owlmeans/context'

export const makeTestCtx = (overrides: Partial<BasicConfig> = {}): BasicContext<BasicConfig> =>
  makeBasicContext({
    ready: false,
    service: '<pkg>-tests',
    type: AppType.Backend,
    services: {},
    ...overrides,
  })
```

`BasicConfig` is `{ ready, service, type }` plus the optional `alias`, `services`, `debug` and config records — a fixture that names anything else is describing a config field that does not exist.

One factory builds the context and the `append*` mixins the package documents go straight after it, in the same helper — the same shape a real app's `makeContext` has. Nothing is stored for re-creation and no spec builds a second context to register something late.

Specs import `makeTestCtx()` (or whatever helper fits) and never call `makeBasicContext` directly. This keeps the wiring centralised so a context-shape change only touches one file per package.

## Spec shape

```ts
import { describe, expect, test } from 'bun:test'
import { makeTestCtx } from './context.js'

describe('<package> — <area>', () => {
  test('does the thing the SKILL.md documents', () => {
    const ctx = makeTestCtx()
    /* exercise the real public API */
  })
})
```

Use `bun:test`'s `describe`/`test`/`expect`. No `vi.mock`, no `jest.mock`, no test doubles.

## Rules

- **Max 3-4 tests per method/function.** Cover the happy path, the documented edge cases, and at most one invariant.
- **Test services, components, helpers, broad domain models.** Test domain models only when the model has functionality the service facade does not expose.
- **Skip utils** (`src/utils/*`) — they are internal.
- **Skip types** (`src/types.ts`).
- **Cover `SKILL.md` and `README.md` first.** Tests are executable docs of the documented use cases.
- **No mocks.** If a test seems to need one, the package likely belongs in category C (integration) or the test is asking the wrong question.
- Cross-package imports are normal `import` statements; the workspace links resolve them.

## Wiring `bun test`

Per-package `package.json`:

```json
"scripts": {
  "test": "bun test ./tests"
}
```

`bun:test` is built-in, so `@types/bun` is the only devDep a plain category-A package needs; add
`@owlmeans/test` when a spec actually uses a helper above. Add `"./tests/**/*"` to the package's
`tsconfig.json` `exclude` so `tsc -b` never compiles specs into `build/`. The root `bun run test`
script runs every package's `test` script via workspace filters.

## When auth shows up

If a category-A package starts to need an authenticated identity to exercise a behaviour, that behaviour belongs in a different package — the auth-aware sibling. Don't reach for `@owlmeans/test-auth` here. Move the test to category B (and possibly the implementation) instead of dragging an auth dep into a low-layer package.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…