---
title: "Conditional Test Logic"
type: "test-smell"
slug: "conditional-test-logic"
url: "http://localhost:3000/en/test-smells/conditional-test-logic.md"
category: "Obscure Smells"
description: "A test that uses `if`/`switch`/ternaries, loops, or `try`/`catch` to decide what to run or assert, so its behavior — and whether it verifies anything at all — depends on which branch executes at runtime."
---
# Conditional Test Logic

> A test that uses `if`/`switch`/ternaries, loops, or `try`/`catch` to decide what to run or assert, so its behavior — and whether it verifies anything at all — depends on which branch executes at runtime.

## Signs and Symptoms

A test reads like a small program instead of a linear "arrange → act → assert" script. Look for control flow inside the test body:

* `if`/`else`, `switch`, ternaries, or `&&`/`||` short-circuits that gate which assertions run.
* `for`/`while`/`forEach` loops that build inputs or iterate over assertions.
* `try`/`catch` used to "test" an error path, with `expect` calls hidden inside the `catch`.
* One test reused for several cases by branching on a flag or on environment state.
* Expected values computed in the test (often with a loop or formula) instead of hard-coded.

```js
// Smell: assertions live inside conditional/branching code
test('user discount', () => {
  const user = getUser();
  if (user.isPremium) {
    expect(price(user)).toBe(80);   // may never run
  } else {
    expect(price(user)).toBe(100);  // may never run
  }
});

test('throws on bad input', () => {
  try {
    parse('!!!');
    // if parse() does NOT throw, we fall through and assert nothing → test passes
  } catch (err) {
    expect(err.message).toMatch(/invalid/);
  }
});

```

The tell-tale failure mode: the test stays green even when the code is broken, because the branch containing the assertion was never taken.

## Reasons for the Problem

**Why it happens**

* **DRY taken too far.** Authors try to cover several scenarios with a single "flexible" test, branching on inputs or a flag instead of writing one test per case (Meszaros: _Flexible Test_).
* **Environment coupling.** The SUT wasn't decoupled from its dependencies, so the test adapts to whatever state it finds at runtime.
* **Defensive teardown.** `if (resource) resource.close()` creeps in to avoid tearing down fixtures that may not exist (_Complex Teardown_).
* **Computed expectations.** The expected result is derived with the same algorithm as production code, dragging that logic — loops and all — into the test (_Production Logic in Test_).
* **Error-path testing by hand.** `try`/`catch` is used to assert on a thrown error instead of a built-in matcher.

**Why it hurts**

* **False confidence (the worst part).** Most runners only fail a test when an assertion throws. If the asserting branch is skipped — or the code under test doesn't throw inside a `try` — the test passes having verified _nothing_.
* **Untested test code.** Branches and loops in a test are logic that itself has no tests; a bug in the test's own control flow goes unnoticed.
* **Poor diagnostics.** When a branching test fails you must first work out _which_ path ran before you can interpret the failure.
* **Reduced readability & maintainability.** A linear test documents one behavior with one expected outcome; a branching test forces the reader to simulate execution to know what is actually guaranteed.
* **Brittleness.** Tests that key off runtime/environment state pass or fail non-deterministically.

## Treatment

Make each test a single, unconditional, linear path. Concretely:

1. **One scenario per test.** Split a branching test into separate tests, or use the framework's data-driven API (`test.each`, `it.each`, parameterized tests) so each case is its own clearly-named, independently-reported run.
2. **Hoist branching outside the test.** If a case only applies under some condition, decide that at _definition_ time (e.g. `describe`/`it` chosen by config), not inside the test body — the assertions themselves stay unconditional.
3. **Hard-code expected values.** Replace computed expectations with literal expected results (or an _Expected Object_ / custom matcher). Don't re-implement production logic in the test.
4. **Test error paths with matchers,** not `try`/`catch`: `expect(fn).toThrow(...)`, `await expect(p).rejects.toThrow(...)`. These fail loudly when no error is thrown.
5. **If a conditional assertion is truly unavoidable,** pin the count with `expect.assertions(n)` / `expect.hasAssertions()` so a skipped branch fails instead of silently passing.
6. **Replace conditional teardown** with framework lifecycle hooks (`afterEach`) and automatic/idempotent cleanup, so no `if` is needed to guard teardown.

```js
// Before — branching test, assertions may be skipped
test('user discount', () => {
  const user = getUser();
  if (user.isPremium) expect(price(user)).toBe(80);
  else                expect(price(user)).toBe(100);
});

// After — one explicit case per row, every assertion always runs
test.each([
  ['premium', { isPremium: true },  80],
  ['regular', { isPremium: false }, 100],
])('price for %s user', (_label, user, expected) => {
  expect(price(user)).toBe(expected);
});

// Before — try/catch that passes when nothing throws
try { parse('!!!'); } catch (e) { expect(e.message).toMatch(/invalid/); }

// After — fails if parse() does not throw
expect(() => parse('!!!')).toThrow(/invalid/);

```

## Detected by

- **eslint-jest** `jest/no-conditional-expect` — no-conditional-expect (https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/no-conditional-expect.md)
- **eslint-jest** `jest/no-conditional-in-test` — no-conditional-in-test (https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/no-conditional-in-test.md)
- **eslint-vitest** `vitest/no-conditional-expect` — no-conditional-expect (https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-conditional-expect.md)
- **eslint-vitest** `vitest/no-conditional-in-test` — no-conditional-in-test (https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-conditional-in-test.md)
- **eslint-vitest** `vitest/no-conditional-tests` — no-conditional-tests (https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-conditional-tests.md)
