When several tests have the same structure and differ only by a few hardcoded values, they are harder to maintain as separate tests. A table-driven test makes the common behavior explicit and keeps the varying data in one place.
The right balance needs to be found. There is no point in grouping tests when the parameterized version is more complex than the original tests.
For JavaScript and TypeScript, this rule targets Jest-, Vitest-, and Playwright-compatible test and it APIs. It raises an
issue when at least three tests in the same scope can be refactored as one parameterized test.
To avoid recommending a table-driven test that is harder to read than the original tests, this rule uses a conservative heuristic. It only reports tests with the same statement structure, where the differences are literal values of the same kind, for example strings with strings or numbers with numbers. It reports only when the common structure still contains duplicated statements after the varying literals are parameterized, and when no more than three literals need to become parameters.
import { test, expect } from "vitest";
test("describes Europa", () => { // Noncompliant
const moon = describeMoon("europa");
expect(moon.kind).toBe("moon");
expect(moon.planet).toBe("Jupiter");
expect(moon.label).toBe("Europa");
});
test("describes Io", () => {
const moon = describeMoon("io");
expect(moon.kind).toBe("moon");
expect(moon.planet).toBe("Jupiter");
expect(moon.label).toBe("Io");
});
test("describes Ganymede", () => {
const moon = describeMoon("ganymede");
expect(moon.kind).toBe("moon");
expect(moon.planet).toBe("Jupiter");
expect(moon.label).toBe("Ganymede");
});
import { test, expect } from "vitest";
test.each([
["europa", "Europa"],
["io", "Io"],
["ganymede", "Ganymede"],
])("describes %s", (slug, label) => {
const moon = describeMoon(slug);
expect(moon.kind).toBe("moon");
expect(moon.planet).toBe("Jupiter");
expect(moon.label).toBe(label);
});
import { it, expect } from "@jest/globals";
it("rejects a missing user id", () => { // Noncompliant
const response = validateUser({ id: null });
expect(response.valid).toBe(false);
expect(response.error).toBe("id is required");
});
it("rejects a negative user id", () => {
const response = validateUser({ id: -1 });
expect(response.valid).toBe(false);
expect(response.error).toBe("id must be positive");
});
it("rejects a non-numeric user id", () => {
const response = validateUser({ id: "abc" });
expect(response.valid).toBe(false);
expect(response.error).toBe("id must be a number");
});
import { it, expect } from "@jest/globals";
it.each([
[null, "id is required"],
[-1, "id must be positive"],
["abc", "id must be a number"],
])("rejects invalid user id %p", (id, error) => {
const response = validateUser({ id });
expect(response.valid).toBe(false);
expect(response.error).toBe(error);
});
import { test, expect } from "@playwright/test";
test("opens the planets page", async ({ page }) => { // Noncompliant
await page.goto("/planets");
await page.waitForLoadState("networkidle");
await expect(page.getByRole("heading", { name: "Planets" })).toBeVisible();
});
test("opens the moons page", async ({ page }) => {
await page.goto("/moons");
await page.waitForLoadState("networkidle");
await expect(page.getByRole("heading", { name: "Moons" })).toBeVisible();
});
test("opens the comets page", async ({ page }) => {
await page.goto("/comets");
await page.waitForLoadState("networkidle");
await expect(page.getByRole("heading", { name: "Comets" })).toBeVisible();
});
import { test, expect } from "@playwright/test";
[
["/planets", "Planets"],
["/moons", "Moons"],
["/comets", "Comets"],
].forEach(([path, heading]) => {
test(`opens the ${heading} page`, async ({ page }) => {
await page.goto(path);
await page.waitForLoadState("networkidle");
await expect(page.getByRole("heading", { name: heading })).toBeVisible();
});
});
test.each(table)(name, fn, timeout)test.each