Skip to content

boneskull/bupkis

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

83 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

BUPKIS: The Glory of Certainty

⁓ BUPKIS ⁓

Uncommonly extensible assertions for the beautiful people
by @boneskull

Quick Links

Motivation

"Another assertion library? You cannot be serious. 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! Codswallop!"

‒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?, ...]);
  • One more convention worth mentioning is negation.

    You can negate just about any phrase by prepending it with not and a space. 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/,
    );

    Handy!

Custom Assertions Built With Zod

Zod is a popular object validation library which does some heavy lifting for BUPKIS—most of the built-in assertions are implemented using Zod schemas. And so BUPKIS extends this capability to you.

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');

📒 Registration?

"Registration" of an assertion (though there is no stateful "registry" anywhere) is as straightforward as passing an array of Assertion instances (created via createAssertion()/createAsyncAssertion()) to the use() function.

use(), as exported from bupkis, returns a new expect()/expectAsync() pair that includes your custom assertions alongside all the built-in ones. The new expect()/expectAsync() functions are fully type-safe and aware of your custom assertions. They also have a .use() method, which allows you to compose sets of assertions from disjoint sources.

Zod makes it extremely easy to create most custom assertions. But despite its power, it 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() or expectAsync().
  • Every custom assertion is also fully type-safe and is declared as an overload for expect() or expectAsync() (as returned from use())
  • 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 a Map as 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 has a peer dependency on Zod v4+, but will install it as an optional dependency if you are not already using it.

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 -D

Usage

👉 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.
  • Zod is a popular object validation library upon which BUPKIS builds many of its own assertions.
  • 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!
  • tshy from Isaac Schlueter. 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:test that 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.

About

Uncommonly extensible assertions for the beautiful people

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published

Contributors 3

  •  
  •  
  •