skip to content

What is a data-driven test, and why does each row need its own case name?

level: juniorimportance: must knowfreq 71%

answer

  1. logic once, inputs many
  2. the table is input, not code
  3. a failure should name the row
  4. one case per row, not per table
  5. label column, stable and unique

basics

~20 s

A data-driven test runs one piece of logic over many input rows held in a table or file. Each row should execute as its own named case, so a failure names the offending row instead of the whole test.

solid answer

~40 s

A data-driven test pulls the inputs and expected outcomes out of the code and into a table, a file or a store, and runs the same logic once per row. Adding coverage becomes a data edit rather than a code edit. The naming matters because the report is the only thing the reader gets: if all 47 rows execute inside one case body, the first failing row aborts the run, the rows after it never execute, and the report says only that the test failed. Expanding the table into one case per row, each named from a short label column, gives a per-row result, a per-row history and a failure message that identifies itself. The table holds inputs and expected outcomes only — assertions, waiting and any branching stay in code.

code

pseudocode · 11 lines
pseudocode
table = load_table("signing_invitations.tbl")

for row in table:
    define_case(name = row.label) as:          # "witness/resend-within-expiry/5d"
        envelope = create_envelope(template = "nda-v4")
        send_invitation(envelope,
                        role = row.role,
                        channel = row.channel,
                        expiry_days = row.expiry_days)
        actual = count_audit_entries(envelope, "INVITATION_SENT")
        assert actual == row.expected_invitations

go deeper

for a junior

Be ready to define it in one sentence and to say where the inputs live. Know that each row should appear as its own result in the report, and that a row carries both inputs and the expected outcome.

for a middle

Explain the mechanics: the table is expanded into discrete cases before execution, names come from a label column, and a loop inside a single case aborts at the first failure so later rows never run.

for a senior

Show judgement about what belongs in the table. Argue against control-flow columns and cross-product row growth, and talk about naming that stays stable across edits so per-row failure history remains meaningful.

for a principal

Own the tradeoff between coverage breadth and feedback budget. Be able to say when a large table should be split by risk, sampled, or moved to a slower tier rather than allowed to dominate the pipeline.

## Two things that beginners keep together A data-driven test separates the **logic** of a check from the **inputs and expected outcomes** it runs against. The logic is written once, in code. The inputs live outside it — a table embedded in the case file, a delimited file, a structured data file, a sheet exported by an analyst, or rows read from a store — and the runner executes that one piece of logic once per row. Moving the data out changes three things. Adding a case becomes an edit to data rather than an edit to code, so someone who is not fluent in the codebase can extend coverage. The same logic gets exercised across a wide input space, so drift in the behaviour shows up as *one row* failing rather than as a rewrite of a hand-written case. And the report becomes the contract with whoever reads it: if the report cannot say which row broke, the whole arrangement has bought you nothing. ## Why each row must be its own case Two implementations look equivalent and are not. In the first, a single case loads the table and loops over the rows inside its own body. The first row that fails aborts the case; every remaining row is never executed. The report shows one case and one failure. Unless the failure message was hand-built to include the row, the reader is told that "the invitation rules test failed" and nothing more. In the second, the table is expanded *before* execution into one discrete case per row, each with a display name derived from that row. The report shows as many results as there are rows, failures are counted per row, and a run history can track a single row's stability over time. A worked example. A suite covers the signing-invitation step of a document e-signing flow. The table carries 47 rows with four input columns — signer role (sender, counterparty, witness), delivery channel, invitation expiry in days — and one expected-outcome column: the number of `INVITATION_SENT` entries the envelope's audit trail should hold afterwards, normally one. Three rows fail. The witness role, re-invited inside the expiry window, records **two** invitation entries instead of one: a duplicated side effect. With one case per row, the report prints "witness / resend-within-expiry / 5-day expiry — expected 1 invitation entry, found 2" three times, and the shared factor is visible in seconds. With the loop-inside-one-case shape, the run reports a single failure on row 12, rows 13 through 47 never execute, and the team spends part of a 3-week release train believing it has 47 rows of evidence when it has 12. The unexecuted mass is the real damage; the missing row name is only the first symptom. ## Naming a row so the failure names itself The case name is the diagnostic surface, so it deserves design. * **Give the table a label column** rather than deriving the name from the raw inputs. A hand-written label such as `witness/resend-within-expiry/5d` says what the row is *for*; a name auto-built from every column is long, changes whenever an unrelated column changes, and is unreadable in a report. * **Keep it unique.** Two rows sharing a name collapse in most reporting formats, and one of the two results silently wins. * **Keep it stable across runs.** A name built from a row index shifts the moment someone inserts a row; every stability record attached to the old name now points at a different case. A name built from meaning survives edits. * **Keep secrets and personal data out of it.** The name travels into reports, dashboards and tickets — none of which are as protected as the test data itself. * **Keep it short.** It is a title, not a description; the detail belongs in the failure message. ## What belongs in the table, and what does not Inputs and the expected outcome for those inputs. That is the whole list. The assertion *mechanism*, the waiting, the environment wiring and the control flow stay in code. A column that reads "if the role is witness, skip the audit check" is control flow that has leaked into data: it is now a conditional written in a notation with no editor support, no review tooling that understands it, and no way to test it. When a table starts sprouting such columns, the honest move is to split it into two tables with two pieces of logic. An expected-outcome column is not optional. A row that supplies only inputs and asserts that nothing threw is a row that can only fail on a crash; it will pass through almost any behavioural regression in the flow it claims to cover. ## Rows are not free Every row costs run time, and rows that differ only cosmetically add minutes and no information. Choosing rows deliberately — boundaries, representative classes, the combinations that a real defect history says matter — beats generating a cross-product of every column and letting the suite grow until it no longer fits its feedback budget. A table that has quietly become the slowest thing in the pipeline is a design decision nobody made. ## The cost you have taken on The compensation for all this is that the failure no longer points at a line of code you wrote for this case; it points at a generic body and a row. That price is manageable while the indirection is one level deep — data outside, logic inside — and it grows sharply once the *steps* move out too.

  • Which columns belong in the input table, and which parts must stay in the code?
    Inputs and the expected outcome for those inputs belong in the table. The assertion mechanism, any waiting, the environment wiring and all branching stay in code. A column such as "skip the audit check when the role is witness" is control flow that has leaked into data; it has no editor support and no review tooling, and the right fix is two tables with two pieces of logic rather than a conditional column.
  • How would you name a row when its inputs are long, sensitive, or change often?
    Add a short hand-written label column rather than deriving the name from every input. A label states what the row is for, survives edits to unrelated columns, and keeps personal data and secrets out of reports, dashboards and tickets. Keep it unique, since duplicate names collapse in most report formats, and keep it stable, because per-row stability history is keyed on the name.
  • A table grows to several thousand rows and the suite no longer fits its feedback budget. What do you do?
    Treat row count as a cost, not a virtue. Cut rows that differ only cosmetically, keep boundaries and the combinations a real defect history justifies, and split the remainder by risk so a small set runs on every change and the long tail runs on a slower schedule. Growth by cross-product is a design decision nobody made, and it should be reversed rather than absorbed.

It is the difference between a payroll report that says "payroll failed" and one that says which employee's row failed — same calculation, but only the second one can be acted on.

saying these in an interview costs you the question

  • Thinks data-driven testing means testing the database layer
  • Loops every row inside one case and calls that parameterised
  • Leaves all cases sharing one name, so failures are indistinguishable
  • Puts control flow, such as a skip-if column, into the table
  • Adds rows by cross-product because rows look free
  • Ships rows with inputs but no expected-outcome column

context