---
title: "Assertion Roulette"
type: "test-smell"
slug: "assertion-roulette"
url: "http://localhost:3000/en/test-smells/assertion-roulette.md"
category: "Assertion Smells"
description: "A test packs many undocumented assertions into one method, so when it fails you can't tell which assertion fired or why — you have to gamble."
---
# Assertion Roulette

> A test packs many undocumented assertions into one method, so when it fails you can't tell which assertion fired or why — you have to gamble.

## Signs and Symptoms

A single test contains a string of bare assertions with no explanatory messages and no clear single objective. When it goes red, the failure report (especially a CI summary line or a shared assertion helper) tells you _that_ something broke, but not _which_ check or _why_ — you play "roulette" to find the culprit.

Tell-tale signs:

* Many `expect(...)` / `assert(...)` calls in one test, none carrying a descriptive message or label.
* The test name is generic (`'works'`, `'user profile'`, `'test register'`) so a failure communicates nothing on its own.
* Assertions span several unrelated concerns (an [Eager Test](https://en.wikipedia.org/wiki/Test%5Fsmell) reusing one fixture to check everything at once).
* A failure makes you reach for the debugger or line numbers to figure out what was actually being verified.
* Because most asserts fail-fast, the first failure short-circuits the rest, so you never see the later checks in a single run.

```js
test('user profile', () => {
  const user = createUser({ name: 'Ada', age: 36 });
  expect(user.name).toBe('Ada');
  expect(user.age).toBe(36);
  expect(user.isAdult).toBe(true);   // if THIS is the red one,
  expect(user.slug).toBe('ada');     // the report just says
  expect(user.roles).toContain('member'); // "expected false to be true"
  expect(user.createdAt).toBeInstanceOf(Date);
});

```

Note: modern runners (Jest, Vitest) print the failing line and a diff, which softens the "which line?" problem. The smell still bites when the test mixes objectives, has a vague name, loops over assertions, hides them behind shared helpers, or runs where only a summary message survives.

## Reasons for the Problem

**Why it happens**

* _Eager Test / fixture reuse._ Expensive setup tempts you to pile on "while I'm here" checks rather than write a second test.
* _Whole-object verification done the hard way._ Asserting an object field-by-field naturally produces a long run of undocumented asserts.
* _Copy-paste growth._ Tests accrete assertions over time without anyone splitting them.
* _Historical tooling._ Classic xUnit assertions reported only pass/fail, so an unlabeled assert gave no context on failure — the origin of Meszaros's original smell.
* _Deadline pressure._ One big test feels faster to write than several focused ones.

**Why it hurts**

* _Diagnosability / reliability._ Per testsmells.org, "multiple assertion statements in a test method without a descriptive message impacts readability/understandability/maintainability as it's not possible to understand the reason for the failure." You burn time locating which assertion fired.
* _Hidden defects (false confidence)._ Fail-fast assertions stop at the first failure, so later assertions never execute. You fix one, rerun, find the next — multiple bugs are masked, and a "green after one fix" feels safer than it is.
* _Readability._ The test stops documenting a single behavior; its intent is buried in a list, and a generic test name adds nothing.
* _Maintainability._ It's unclear whether a new check belongs in this test or a new one, so the pile keeps growing and unrelated concerns become coupled.

## Treatment

Aim for the principle behind a Single-Condition Test: **each test verifies one behavior and can fail for exactly one reason.**

1. **Split by objective.** Break the omnibus test into focused tests with descriptive names. The name then _is_ the failure message.
2. **Collapse field-by-field checks into one assertion.** Use `toEqual` / `expect.objectContaining` / a reviewed snapshot so N asserts become one meaningful diff.
3. **When grouping is genuinely cohesive, label the assertions.** Jest/Vitest `expect` has no message arg, so use `node:assert`'s message parameter, Vitest's `expect(actual, message)`, or `jest-expect-message`.
4. **Want every check to run and report together?** Prefer splitting; otherwise use soft assertions (`expect.soft` in Vitest) so one failure doesn't hide the rest.
5. **Guard against regression** by capping assertions per test (see detectors).

Before — assertion roulette:

```js
test('register user', () => {
  const res = register({ email: 'a@b.com', age: 36 });
  expect(res.ok).toBe(true);
  expect(res.user.email).toBe('a@b.com');
  expect(res.user.isAdult).toBe(true);
  expect(res.welcomeEmailSent).toBe(true);
});

```

After — one behavior per test, with a whole-object assert:

```js
describe('register', () => {
  it('accepts a valid adult signup', () => {
    expect(register({ email: 'a@b.com', age: 36 }).ok).toBe(true);
  });

  it('stores the normalized user record', () => {
    const { user } = register({ email: 'a@b.com', age: 36 });
    expect(user).toEqual(
      expect.objectContaining({ email: 'a@b.com', isAdult: true }),
    );
  });

  it('sends a welcome email on signup', () => {
    expect(register({ email: 'a@b.com', age: 36 }).welcomeEmailSent).toBe(true);
  });
});

```

If you must keep one test, make every assertion self-describing:

```js
import { strict as assert } from 'node:assert';
assert.equal(res.ok, true, 'registration should succeed');
assert.equal(res.welcomeEmailSent, true, 'welcome email should be sent');

```

## Detected by

- **eslint-jest** `jest/max-expects` — Flags the count-based proxy of Assertion Roulette: reports when a test exceeds N expect() calls (default 5). Docs note that more assertions tend to mix multiple objectives. (https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/max-expects.md)
- **eslint-vitest** `vitest/max-expects` — Vitest equivalent: enforces a maximum number of expect() assertions per test, catching tests that pack in too many checks. (https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/max-expects.md)
- **sonar** `java:S5961` — SonarSource 'Test methods should not contain too many assertions' — caps assertions per test (default 25 for JUnit/AssertJ), the standard static-analysis proxy for this smell. (https://rules.sonarsource.com/java/RSPEC-5961/)
