As a data-driven JUnit 5 test suite grows, how do you decide between plain indexed parameters, an ArgumentsAccessor read inside the test body, and a custom ArgumentsAggregator — and what is the cost of getting that call wrong?
answer
- narrow rows → indexed parameters
- one-off wide/variable rows → accessor
- recurring shape or real construction → aggregator
- indirection vs unnamed indices
- wide row = missing type or over-broad test
basics
~20 sIndexed parameters while the row is narrow and each column is a named concept. ArgumentsAccessor for one-off wide or variable-arity rows. A custom aggregator when the same shape recurs or construction is non-trivial. Wrong call costs either unreadable signatures or indirection that hides column meaning.
solid answer
~60 sI treat it as a readability-versus-reuse curve. - **Indexed parameters** are the default: names in the signature, compile-time types, zero indirection. They stop scaling around four or five columns, or when several columns are really one concept. - **`ArgumentsAccessor` in the body** buys flexibility for a single test — a wide row, a row whose arity varies, feeding a builder. It costs named columns and moves type errors to runtime, so I keep it local and add column constants. - **A custom `ArgumentsAggregator`** pays off when the same row shape appears in several tests, when construction has defaults or validation, or when the signature should read as domain language via a composed annotation like `@CsvToOrder`. The failure modes are symmetric. Too little aggregation gives twelve-parameter signatures nobody can diff. Too much gives tests whose inputs are invisible without opening another class, and an aggregator quietly coupled to CSV column order that breaks at runtime after a data edit. When rows get so wide that only an aggregator can tame them, I first ask whether the *test* is doing too much, or whether the production code is missing the very value object the aggregator is building.
go deeper
Know the three options exist and that indexed parameters are the default for small rows.
Give concrete tipping points — column count, repeated shape, non-trivial construction — and name the cost of each option.
Argue both failure modes, insist that aggregators translate rather than repair, and describe how you would refactor an over-accessorised suite.
Treat wide rows as a signal about test scope or a missing domain type, and define team-wide conventions that keep a large data-driven suite legible.
## Framing the decision All three options move the same information from an argument row into the test body. They differ in where the mapping from column position to meaning lives: - **Indexed parameters** — the mapping lives in the method signature. Best-case readability: `void t(String sku, int qty, BigDecimal expected)` needs no other file to understand. - **`ArgumentsAccessor`** — the mapping lives in the test body as literal indices. Maximum flexibility, minimum names. - **`ArgumentsAggregator`** — the mapping lives in a separate reusable class, and the signature carries a domain type. The engineering judgment is about *where a future reader will look* and *what breaks when the data changes*. ## When indexed parameters are right Up to roughly four or five columns, each a distinct named concept, indexed parameters win on every axis: names, compile-time types, no indirection, refactoring support from the IDE. Do not add machinery here. A signature of three parameters is not a problem to solve. They become wrong when (a) the count climbs past what a reader can hold, (b) several columns are obviously one thing — first name, last name, date of birth, all describing one person — or (c) adding a column forces a mechanical edit across many methods. ## When the accessor is right The accessor is the flexible middle. Use it when: - the row is wide but the test is one-off, so a reusable class would be ceremony; - rows have *different arities* and the test branches on `size()`; - the arguments are genuinely a collection ("assert all of these parse"), not distinct concepts; - you are feeding a builder and want to keep the assembly visible in the test. Its cost is real: `args.getString(7)` names nothing, and a column reorder breaks it silently at runtime. Mitigate with `private static final int` column constants and by keeping the accessor use inside one test rather than sprinkled through a class. ## When a custom aggregator is right Promote to an `ArgumentsAggregator` when at least one of these holds: - **Recurrence** — three or more tests read the same row shape. The aggregator becomes the single definition of that shape. - **Non-trivial construction** — defaults, validation, a nested object graph, several columns feeding one constructor argument. - **Signature clarity** — you want `void t(@CsvToOrder Order order, BigDecimal expected)` so the test reads as domain language. - **Consistent failure reporting** — one place to throw `ArgumentsAggregationException` with the offending column named, so data problems never masquerade as production bugs. Its cost is indirection: the reader must open the aggregator to learn what column 3 means, and the aggregator is coupled to column order with no compile-time protection. Keep aggregators tiny, keep them near their tests, and name the composed annotation for the source shape it consumes. ## The costs of getting it wrong *Under-aggregating* produces signatures that no code review can meaningfully diff, and every added column touches every test. It also tends to produce copy-pasted assertions because nobody wants to widen one more method. *Over-aggregating* is the subtler failure. The test body no longer shows its inputs; understanding a failing case means reading the CSV, then the aggregator, then reconstructing the object mentally. Worse, an aggregator that validates or defaults can hide bad data: a column silently defaulted to zero makes a test pass while asserting nothing. I treat "the aggregator repairs the input" as a defect — aggregators translate, they do not fix. ## The question behind the question When a row grows past ten columns, the first thing I ask is not which aggregation mechanism to use, but *why the test needs ten inputs*. Two answers recur: - The **test is doing too much** — it exercises several behaviours per row and should be split, at which point each split test has a narrow row and indexed parameters again. - The **production model is missing a type** — the aggregator is building a `PricingRequest` that the production code should already accept as one object. In that case the honest fix is to introduce that type in production code, after which conversion (a single-`String` factory or a converter) or a much smaller aggregator suffices. That framing matters at a senior/principal level: aggregation machinery is often a symptom of test or model design, and reaching for it reflexively institutionalises the smell. ## Team conventions worth setting - Indexed parameters up to a stated column count; accessor only inside a single test; aggregator only when reused or non-trivial. - Aggregators are pure translation — no defaulting, no repairing, explicit `ArgumentsAggregationException` on structural problems. - Composed annotations named after the shape (`@CsvToOrder`), not after the mechanism. - Column constants wherever raw indices survive. The conventions matter more than which option any individual test picks, because they keep a large data-driven suite legible to people who did not write it.
- You inherit a suite where every parameterized test takes an ArgumentsAccessor. What is your first move?Find the recurring row shapes and extract one aggregator per shape with a composed annotation, starting with the shape used most. That converts anonymous indices into a named type and gives one place to fix when columns change. I would leave genuinely one-off accessor uses alone rather than manufacturing single-use aggregator classes.
- How do you keep an aggregator from hiding bad test data?Forbid defaulting and repair inside aggregators: they translate columns into an object and nothing more. Structural problems throw ArgumentsAggregationException naming the column, and conversion failures propagate as-is. That way a malformed row fails loudly instead of producing a green test that asserted a substituted value.
It mirrors choosing between a long parameter list, an Object[], and a parameter object in production code — same tradeoffs, same tipping points.
saying these in an interview costs you the question
- Treating aggregation as an unconditional best practice and converting readable two-parameter tests
- Letting an aggregator apply defaults or repair invalid rows, so tests pass on data that should fail
- Creating a single-use aggregator class for one test just to avoid an accessor
- Never questioning why a row needs a dozen columns in the first place
- Assuming an aggregator gives compile-time safety against a CSV column reorder