bun:test/valid-expect

Require expect to get a value, end in a called matcher, and be awaited when it is async

error by defaulthas suggestions

expect(value) doesn't check anything by itself. The check happens when a matcher such as .toBe() is called, so an expect with no matcher, or with a matcher that isn't called, passes no matter what the value is.

.resolves and .rejects make the matcher return a promise. If the test doesn't await or return it, the test can end first, and a failed assertion doesn't fail the test.

This rule is on by default in files that import from "bun:test".

Configure#

On by default as an error. To turn it off:

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

Every option, set to its default:

bunfig.toml
[lint.rules]
"bun:test/valid-expect" = ["error", {
  alwaysAwait = false,
  asyncMatchers = ["toResolve", "toReject"],
  minArgs = 1,
  maxArgs = 1,
}]

Examples#

example.ts4 problems
import { expect, test } from "bun:test";

test("adds the tax", () => {
  expect(total(100)); // no matcher
  expect(total(100)).toBe; // the matcher is not called
  expect().toBe(108); // no value
  expect(total(100)).never.toBe(0); // `never` is not a modifier
});
example.ts1 problem
import { expect, test } from "bun:test";

test("loads the user", () => {
  expect(loadUser(1)).resolves.toEqual({ id: 1 });
});
example.tsNo problems
import { expect, test } from "bun:test";

test("adds the tax", () => {
  expect(total(100)).toBe(108);
  expect(total(100)).not.toBe(0);
});

test("loads the user", async () => {
  await expect(loadUser(1)).resolves.toEqual({ id: 1 });
});

test("rejects an unknown id", () => {
  return expect(loadUser(-1)).rejects.toThrow("not found");
});

A second argument is the message to print on failure, and it doesn't count toward maxArgs. expect().pass() and expect().fail() don't need a value:

example.tsNo problems
import { expect, test } from "bun:test";

test("adds the tax", () => {
  expect(total(100), "after tax").toBe(108);
});

test("never calls back", () => {
  subscribe(() => expect().fail("called back"));
});

A missing matcher or a missing await is only reported when the expect is its own statement. If you keep it in a variable or pass it to a function, it can get its matcher or its await somewhere else:

example.tsNo problems
import { expect, test } from "bun:test";

test("adds the tax", () => {
  const result = expect(total(100));
  result.toBe(108);
});

test("loads both users", async () => {
  const first = expect(loadUser(1)).resolves.toEqual({ id: 1 });
  const second = expect(loadUser(2)).resolves.toEqual({ id: 2 });
  await Promise.all([first, second]);
});

Options#

OptionDefaultWhat it does
alwaysAwaitfalseReport every async assertion that isn't awaited where it's written, even one that's returned or kept in a variable.
asyncMatchers["toResolve", "toReject"]Matchers that return a promise, so they must be awaited or returned. Your list replaces the default.
minArgs1The fewest arguments expect can take.
maxArgs1The most arguments expect can take.
example.tswith { "alwaysAwait": true }1 problem
import { expect, test } from "bun:test";

test("rejects an unknown id", () => {
  return expect(loadUser(-1)).rejects.toThrow("not found");
});

Use asyncMatchers for matchers that you add with expect.extend() and that return a promise:

example.tswith { "asyncMatchers": ["toResolveWith"] }1 problem
import { expect, test } from "bun:test";

test("loads the user", () => {
  expect(loadUser(1)).toResolveWith({ id: 1 });
});