Package Exports
- bupkis
- bupkis/assertions
- bupkis/diff
- bupkis/guards
- bupkis/package.json
- bupkis/schema
- bupkis/types
- bupkis/util
Readme
⁓ BUPKIS ⁓
“Uncommonly Extensible Assertions for The Beautiful People”
by @boneskull
Quick Links
- BUPKIS' Homepage (https://bupkis.zip)
- Assertion Reference
- Guide: Basic Usage
- Guide: Creating a Custom Assertion
Motivation
"Another assertion library? You cannot be serious, dogg. My test framework has its own assertions!"
‒sickos, probably
Look, we're old wizened experienced knowledgeable and we've written a lot of tests. We've used a lot of assertion libraries. There are ones we prefer and ones we don't.
But none of them do quite what BUPKIS does. We want an assertion library that prioritizes:
- Type safety
- Uncompromisable extensibility
- A small API surface
We can think of several that tick two-thirds of those boxes! But we demand the total package (And You Should Too).
⚠️ Caution!
Assertion libraries tend come in two flavors: chainable or stiff & traditional. But because these styles are likely familiar to you, you may hate BUPKIS.
We want you to like it, yes. But if you don't, we're content with just making our point and asking the following favor of the reader:
Do not confuse familiarity with usability.
The following is a brief overview of the design choices we made to serve these goals.
Natural Language Assertions
In BUPKIS, "natural language" is the means to the end of "a small API surface".
When you're using BUPKIS, you don't write this:
expect(actual).toEqual(expected);Instead, you write this:
expect(actual, 'is', expected);
// or this
expect(actual, 'to be', expected);
// or this
expect(actual, 'to equal', expected);
// or even this
expect(actual, 'equals', expected);
// or yet another way
expect(actual, 'is equal to', expected);
// or believe it or not, even this
expect(actual, 'to strictly equal', expected);If another assertion library wants you to write:
expect(actual).to.be.a('string');Then BUPKIS wants you to write:
expect(actual, 'to be a string');
// it is tolerant of poor/ironic grammar, sometimes
expect(actual, 'to be an string');Can't remember the phrase? Did you forget a word or make a typo? Maybe you also forgot BUPKIS is type-safe?! You'll get a nice squiggly for your trouble.
This isn't black magic. It ain't no cauldron. We're not just throwing rat tails and strings into a function and stirring that shit up.
"Preposterous! Functions. Things of that nature."
‒the reader and/or more sickos
You may wonder how this could this be anything but loosey-goosey senselessness. On the contrary—we have conventions!
Conventions of expect()
To formalize the conventions at a high level:
The first parameter to a BUPKIS assertion is always the subject (def.).
The "string" part of a BUPKIS assertion is known as a phrase. Every expectation will contain at minimum one (1) phrase. As you can see from the above "to be a string" example, phrases often have aliases.
Assertions may have multiple phrases or parameters, but the simplest assertions always look like this:
expect(subject, 'phrase');
...and more complex assertions look like this:
expect(subject, 'phrase', [parameter?, phrase?, parameter?, ...]);
If an assertion's phrase contains something like "to satisfy" or "satisfying", then the next parameter has special meaning. It is somewhat like Jest's
expect.objectContaining(), but more powerful (prior art: unexpected'sto satisfyassertion).There are three (3) things to know about this parameter:
- It matches objects, but only verifies that the keys provided are a) present and the values satisfy the values in the expected object.
- If the value of any given property in an object is a regular expression (
RegExp), the actual value will be coerced to a string and tested against the regular expression. This provides easy string matching within nested properties (see below example for what this looks like). - Any assertion (including custom assertions) can be embedded within an object parameter via the
expect.it()function:
expect(user, 'to satisfy', { name: expect.it('to be a string'), email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, // equivalent to "to match" below age: expect.it('to be greater than', 18), roles: [expect.it('to be a string')], // Each array element must be a string metadata: { lastLogin: expect.it('to match', /^\d{4}-\d{2}-\d{2}$/), preferences: { theme: expect.it('to be one of', ['light', 'dark']), notifications: expect.it('to be a boolean'), }, }, });
👉 See Embeddable Assertions for more a thorough explanation of
expect.it().You can negate any assertion by prepending its first phrase with
not. For example:expect(actual, 'to be', expected); expect(actual, 'not to be', expected); expect( () => throw new TypeError('aww, shucks'), 'not to throw a', TypeError, 'satisfying', /gol durn/, );
You can "chain" multiple assertions (including custom assertions) together by delimiting them with
and:expect( user, 'to satisfy', { name: expect.it('to be a string', 'and', 'to have length less than', 100), age: expect.it('to be greater than', 18), }, 'and', 'to have property', 'email', );
How about them apples.
Custom Assertions Built With Standard Schema V1
BUPKIS supports any Standard Schema V1-compliant validation library, including Zod, Valibot, ArkType, Effect Schema, and many others.
Most of the built-in assertions are implemented using Zod schemas, but you can use any Standard Schema V1 library to create custom assertions. BUPKIS extends this capability to you regardless of your preferred validation library.
An example will be illuminating. What follows is a stupid quick stupid example of a creating and "registering" a basic assertion which can be invoked using two different phrases:
import { z, use, createAssertion } from 'bupkis';
const stringAssertion = createAssertion(
z.string(),
[['to be based', 'to be bussin']],
z.string(),
);
const { expect } = use([stringAssertion]);
expect('chat', 'to be based');
expect('fam', 'to be bussin');
// did you know? includes all builtin assertions!
expect('skiball lavatory', 'to be a string');📒 Assertion Registration?
"Registration" of an assertion (a misnomer; there is no stateful "registry" in BUPKIS) is as straightforward as passing an array of objects created by
createAssertion()and/orcreateAsyncAssertion()to theuse()function.
use(), as exported frombupkis, returns a newexpect()/expectAsync()pair that which can execute your custom assertions in addition to every built-in assertion.The
expect()/expectAsync()functions returned byuse()are fully type-safe and aware of your custom assertions. Eachexpect()/expectAsync()function has a.use()method as well; this allows you to compose multiple sets of assertions together (like from several assertion plugin packages).
Any Standard Schema V1 library makes it extremely easy to create most custom assertions. Whether you prefer Zod, Valibot, ArkType, or another compliant library, they all work seamlessly. But despite their power, validation libraries can't do everything we need an assertion to do; for those situations, there's also a function-based API for use with parametric and behavioral (e.g., involving function execution) assertions.
👉 For an assiduous guide on creating assertions, read Guide: How to Create a Custom Assertion.
Excruciating Type Safety
We have tried to make BUPKIS is as type-safe as possible. To be clear, that is pretty damn possible. This means:
- Every built-in assertion is fully type-safe and is declared as an overload for
expect()orexpectAsync(). - Every custom assertion is also fully type-safe and is declared as an overload for
expect()orexpectAsync()(as returned fromuse()) - If an assertion demands the subject be of a certain type, the TS compiler will squawk if you try to use an incompatible subject type. For example,
<Map> to have size <number>will only accept aMapas the subject, and this will be obvious in your editor.
Note:
expect()is not and cannot be a type guard; see the "Caveats" Reference doc for more information.
Prerequisites
Node.js: ^20.19.0, ^22.12.0, >=23
BUPKIS supports any Standard Schema V1-compliant validation library. It has a peer dependency on Zod v4+ (which implements Standard Schema V1), but will install it as an optional dependency if you are not already using it. You can also use Valibot, ArkType, Effect Schema, or any other Standard Schema V1 library.
BUPKIS ships as a dual CJS/ESM package.
Disclaimer: BUPKIS has been designed to run on Node.js in a development environment. Anyone attempting to deploy BUPKIS to some server somewhere will get what is coming to them.
Installation
npm install bupkis -DUsage
👉 See the Basic Usage Guide for a quick introduction.
📖 Visit https://bupkis.zip for comprehensive guides and reference.
Acknowledgements
- Unexpected is the main inspiration for BUPKIS. However, creating types for this library was exceedingly difficult (and was in fact the first thing we tried). Despite that drawback, we found it exquisitely usable.
- Standard Schema for creating a unified interface that allows BUPKIS to work with any compliant validation library.
- Zod is a popular object validation library upon which BUPKIS builds many of its built-in assertions. Special thanks to Colin McDonnell for implementing Standard Schema V1.
- fast-check: Thanks to Nicholas Dubien for this library. There is no better library for an assertion library to use to test itself! Well, besides itself, we mean. How about in addition to itself? Yes. Thank you!
- zshy from Colin McDonnell. Thanks for making dual ESM/CJS packages easy and not too fancy.
- TypeDoc it really documents the hell out of TypeScript projects.
- @cjihrig and other Node.js contributors for the thoughtfulness put into
node:testthat make it my current test-runner-of-choice.
Why is it called BUPKIS?
TODO: think of good reason and fill in later
License
Copyright © 2025 Christopher Hiller. Licensed under BlueOak-1.0.0.
