Skip to content

Stop hardcoding "11.111.111-1": generating test RUTs with rut.ts

Generate valid Chilean RUTs for tests, fixtures, and seeds in Vitest, Jest, and Playwright with rut.ts `generate()` — no placeholders, no dependencies.

Arrow Software Team
Arrow Software TeamMay 5, 2026 · 6 min read
A pair of white dice on a desk
Tests should not depend on a single hand-picked RUT. Source: Unsplash.

A test suite that hardcodes 11.111.111-1 in every fixture is a ticking bomb. The day someone enables strict mode in validate() — on the API server, in a shared validation library, or in a middleware layer — every test that treated that placeholder as a real identity breaks at once. generate() and calculateVerifier() make it trivial to produce valid, varied RUTs for any layer of your stack. Because rut.ts has zero runtime dependencies, the factory pulls nothing extra into your test dependency tree.

How do I generate a valid Chilean RUT in tests?#

Call generate() from rut.ts inside a factory function (Vitest, Jest, Playwright — same call). It returns a fresh, real RUT on every invocation that passes validate(rut, { strict: true }) — no placeholders, no manual body picking, no extra fixture library. For stable seeds where the value matters for the assertion, pair a fixed body with calculateVerifier() to derive a deterministic real RUT.

The problem with placeholders#

Common repeated-digit patterns — 11111111-1, 22222222-2, 33333333-3 — pass the Modulo 11 check. Their bodies and verifiers are mathematically consistent. That is exactly why they exist as human-memorable stand-ins. They are not valid identities, and the library knows that.

validate("11.111.111-1") returns true. validate("11.111.111-1", { strict: true }) returns false. The strict flag exists precisely to reject these patterns at the final acceptance gate, because a real identity flow should never let a placeholder through.

The problem surfaces gradually. A team writes fixtures against 11111111-1 while the application validator runs in lenient mode. Months later the team turns on strict, and discovers that every integration test, seeding script, and snapshot file that carries the placeholder is now wrong. The failure is the deferred consequence of a decision made at the start of the project.

Snapshot tests over fixture files are the most brittle variant. A golden file generated with 11.111.111-1 throughout will mismatch the moment the application starts rejecting values differently under strict mode. The diff is large and confusing, and the real cause is buried under a cascade of failures.

The fix is not to avoid strict mode. It is to build fixtures that survive it from the start.

generate() for fixture factories#

generate() returns a fresh valid RUT on every call. There is no configuration, no seed to manage, and no chance of returning a placeholder pattern — the function already produces values that pass validate(rut, { strict: true }).

The natural place for it is a factory function that your test suite calls whenever it needs a user, a customer, or any entity identified by a RUT. Keep factories in one file and import them wherever you need them.

TypeScript
// test/factories.ts
import { generate } from "rut.ts";
 
let i = 0;
 
export function makeCustomer(overrides: Partial<Customer> = {}): Customer {
  i += 1;
  return {
    id: `user-${i}`,
    email: `user-${i}@example.test`,
    rut: generate(),
    ...overrides,
  };
}
 
type Customer = { id: string; email: string; rut: string };

The counter i ensures that successive calls produce distinct id and email values within the same test run. The RUT is independently random, so a UNIQUE constraint on the column will not conflict across calls. If you need to override the RUT for a specific assertion, pass it through overrides.

This pattern works identically with Vitest and Jest.

calculateVerifier() for deterministic seeds#

Some tests need stable fixtures: golden-file comparisons, snapshot tests, or cases where the exact RUT value is load-bearing for the assertion. In those situations you want the same RUT on every run, without picking a placeholder.

Pick a body — any body in the valid range — and derive the verifier with calculateVerifier(). The function is pure and deterministic: the same body always produces the same verifier. The resulting RUT is valid, not a placeholder, and will survive strict validation.

TypeScript
import { calculateVerifier } from "rut.ts";
 
const SEED_BODIES = ["10000001", "10000002", "10000003"];
export const SEED_RUTS = SEED_BODIES.map((b) => `${b}-${calculateVerifier(b)}`);

Keep this list in one shared module so every test file that needs stable RUTs pulls from the same source. Add more bodies when you add more test users; keep the list short enough that a developer can read it at a glance. Do not mix these deterministic RUTs with generate() calls in the same fixture — stable and random RUTs serve different purposes, and conflating them makes test failures harder to read.

End-to-end tests#

End-to-end suites running against a real database hit a different problem: parallel workers. When two Playwright or Cypress shards run the same seeding script concurrently, a hardcoded RUT causes both to attempt an INSERT of the same identity into a table with a UNIQUE constraint. One shard wins; the other gets a database error that has nothing to do with the feature being tested.

Calling generate() at seed time gives each shard a distinct RUT. Workers run concurrently without stepping on each other because they are inserting different values. This works reliably across any number of shards without coordination between them.

Pair generated RUTs with a teardown step that removes the row after the test. Long-lived test databases accumulate data quickly when every shard adds new rows on every run. A cleanup hook — afterEach in Playwright's fixture API, or an after block in Cypress — deletes the row by the generated RUT. This keeps the database small and prevents a value from a previous run from interfering with a current one.

Pitfalls#

Do not supply your own random number generator to generate(). The function already returns valid output. If you implement a seeded RNG and use it to derive a body, the math must produce a body in the valid range; if it does not, you will get a structurally invalid RUT that silently fails strict validation in ways that are hard to trace.

Do not reuse a single generated RUT across unrelated tests. Generate one per test, or one per entity per test. A shared RUT creates implicit coupling between tests: a test that deletes its cleanup target might be deleting a row that another test still needs, and the failure appears in the second test rather than the first.

Do not run the strict validator in production and hardcode 11.111.111-1 in the same test suite. The two decisions are in direct conflict. Either the application accepts placeholders — in which case strict mode is off — or it does not, in which case the fixtures must carry real RUTs. Pick one posture and apply it consistently across application code and tests.

Further reading#

rut.ts

MIT License © 2026 Arrow Software