This episode covers building custom matchers and helpers: custom matchers with expect.extend, reusable helper functions, testing domain-specific logic, and sharing helpers across projects.

Jest's built-in matchers cover common needs, but every domain has its own characteristic assertions: "this number is a valid price", "this object is active", or "this string is in UUID format". Episode 16 covers custom matchers & helpers — building custom matchers with expect.extend(), writing reusable helper functions, testing domain-specific logic, and sharing helpers across projects.
With custom matchers, assertions become as expressive as the domain's own language — tests are easier to read, errors are clearer, and domain logic is tested once and reused everywhere.
expect.extend() accepts an object containing matcher functions. Each function receives the value under test and returns an object with pass and message fields:
expect.extend({
toBeDalamRentang(nilai, min, maks) {
const pass = nilai >= min && nilai <= maks;
return {
pass,
message: () =>
`diharapkan ${nilai} dalam rentang ${min}-${maks}, ` +
`tetapi ${pass ? "masuk" : "tidak masuk"} rentang`,
};
},
});
test("usia dalam rentang valid", () => {
expect(25).toBeDalamRentang(18, 60);
});expect(25).toBeDalamRentang(18, 60) is a brand-new assertion you built yourself. The pass field determines the result, and message produces a clear error message when the test fails — far more readable than comparing two separate conditions.
A good matcher validates its arguments and uses expect for composition. If an argument is wrong, throw an error with a helpful message — not a silent failure. Custom matchers can also use this.utils to format values in error messages consistently.
Not all reusable code is a matcher. Regular helper functions — functions that build test data, generate factory objects, or normalize input — are often the better fit:
function buatTransaksi(overrides = {}) {
return {
id: 1,
jumlah: 100000,
status: "sukses",
tanggal: "2026-08-10",
...overrides,
};
}
test("transaksi berstatus sukses", () => {
const t = buatTransaksi();
expect(t.status).toBe("sukses");
});
test("transaksi gagal bisa dioverride", () => {
const t = buatTransaksi({ status: "gagal", jumlah: 0 });
expect(t.jumlah).toBe(0);
});buatTransaksi({ status: "gagal" }) produces a complete object with a single value overridden. This factory pattern eliminates the repetition of assembling test objects and means structural changes only need to be made in one place.
When an assertion repeats the same logic across many tests, turn it into a domain matcher. Here's an example for an e-commerce system:
expect.extend({
toBeDiskonValid(diskon, hargaAsli) {
const pass =
diskon >= 0 &&
diskon < hargaAsli &&
Number.isFinite(diskon);
return {
pass,
message: () =>
`diskon ${diskon} tidak valid untuk harga ${hargaAsli}`,
};
},
});
test("diskon harus lebih kecil dari harga", () => {
expect(50).toBeDiskonValid(100);
});expect(50).toBeDiskonValid(100) tests a business rule: the discount is non-negative, smaller than the price, and a valid number. This matcher captures the discount rule once and reuses it across many tests — a rule change only touches one place.
To make helpers and matchers reusable across many projects, place them in a single shared package:
const matchers = {
toBeDalamRentang: (nilai, min, maks) => {
const pass = nilai >= min && nilai <= maks;
return {
pass,
message: () =>
`diharapkan ${nilai} dalam rentang ${min}-${maks}`,
};
},
};
module.exports = { matchers };This package can be published as an internal npm package or imported from a shared repository. In each project, enable the matchers once in setupFilesAfterEnv:
const { matchers } = require("@tim/shared-jest-matchers");
expect.extend(matchers);require("@tim/shared-jest-matchers") loads the shared matchers, then expect.extend(matchers) registers them. This approach keeps domain assertions consistent across the whole organization without copying code.
Episode 16 covered custom matchers and helpers: building matchers with expect.extend(), writing reusable helper functions with the factory pattern, testing domain-specific logic with expressive matchers, and sharing helpers across projects.
Key takeaways:
expect.extend() creates matchers with an object containing pass and message.setupFilesAfterEnv.In the next episode, episode 17, we'll cover monorepos & multi-project testing — managing Jest in a monorepo structure, multi-project configuration, running tests selectively per package, and centralized versus per-package config decisions.