bun:test/expect-expect

Require every test to make at least one assertion

A test with no assertion passes as long as the code doesn't throw, whether the result is right or wrong. Usually someone forgot the expect, or lost it while changing the test.

Calls to expect and expectTypeOf count as assertions. test.todo and test.skip aren't checked.

Configure#

Off by default. To turn it on:

bunfig.toml
[lint.rules]
"bun:test/expect-expect" = "error" # or "warn"

Every option, set to its default:

bunfig.toml
[lint.rules]
"bun:test/expect-expect" = ["error", {
  assertFunctionNames = ["expect", "expectTypeOf"],
  additionalTestBlockFunctions = [], # list of strings
}]

Examples#

example.ts1 problem
import { test } from "bun:test";

test("parses the port", () => {
  parseConfig("port = 3000");
});
example.tsNo problems
import { expect, test } from "bun:test";

test("parses the port", () => {
  expect(parseConfig("port = 3000")).toEqual({ port: 3000 });
});

test.todo("rejects a negative port");

test.skip("reads the file", () => {
  readConfig("app.conf");
});

Options#

OptionDefaultWhat it does
assertFunctionNames["expect", "expectTypeOf"]The functions that count as an assertion. Your list replaces the default, so keep "expect" in it.
additionalTestBlockFunctions[]Other functions to check like test, such as your own wrappers around it or a hook like "afterEach".

assertFunctionNames#

The rule doesn't look inside the helpers that a test calls. This test is reported even though checkUser calls expect:

example.ts1 problem
import { expect, test } from "bun:test";

function checkUser(user) {
  expect(user.id).toBeGreaterThan(0);
}

test("creates a user", () => {
  checkUser(createUser());
});

Add the helper's name and the test isn't reported anymore:

bunfig.toml
[lint.rules]
"bun:test/expect-expect" = ["error", {
  assertFunctionNames = ["expect", "checkUser"],
}]
example.tswith { "assertFunctionNames": ["expect", "checkUser"] }No problems
import { expect, test } from "bun:test";

function checkUser(user) {
  expect(user.id).toBeGreaterThan(0);
}

test("creates a user", () => {
  checkUser(createUser());
});

A name can be a chain, such as request.get.expect. * matches any characters in one part of the chain, and ** matches any number of parts, including none. Case is ignored.

With "request.**.expect" in the list, this isn't reported:

example.tswith { "assertFunctionNames": ["expect", "request.**.expect"] }No problems
import { test } from "bun:test";

test("is healthy", async () => {
  await request(app).get("/health").expect(200);
});

additionalTestBlockFunctions#

With ["testWithDatabase"], calls to that wrapper need an assertion too:

example.tswith { "additionalTestBlockFunctions": ["testWithDatabase"] }1 problem
import { afterAll } from "bun:test";
import { closeDatabase, testWithDatabase } from "./helpers";

afterAll(closeDatabase);

testWithDatabase("saves the user", (db) => {
  db.save(createUser());
});
example.tswith { "additionalTestBlockFunctions": ["testWithDatabase"] }No problems
import { afterAll, expect } from "bun:test";
import { closeDatabase, testWithDatabase } from "./helpers";

afterAll(closeDatabase);

testWithDatabase("saves the user", (db) => {
  expect(db.save(createUser())).toBe(true);
});

Like every bun:test rule, this one only checks files that import something from bun:test.