Article

Data-Driven Testing: Examples, Frameworks, and Automation

October 21, 2026

Learn how data-driven testing works, design useful datasets, compare frameworks, and automate repeatable checks with clear results for each test case.

Learn how data-driven testing works, design useful datasets, compare frameworks, and automate repeatable checks with clear results for each test case.

Data-driven testing runs the same test logic against many input sets, with an expected result written next to each one. A checkout flow can pass for a $50 cart, but charge the wrong fee at $100. That bug usually sits at a boundary or in a combination nobody tested.

Copying a test for every case gets old fast, and every copy becomes another thing to fix when the page changes. Data-driven testing keeps one copy of the logic and moves the cases into a table. More rows only help when each one checks something the others don't, though.

This guide follows one shipping-fee dataset from the first row to the first failure. It explains how those rows are chosen and what happens when one breaks. The last part compares code-based tools with Functionize Studio, where you write the test in plain language.

What Is Data-Driven Testing?

Data-driven testing is a software testing technique that executes the same test logic using multiple datasets to validate expected application behavior. The steps stay fixed, while each dataset supplies the inputs and expected results. This lets a login test check a valid user and a locked account without duplicating steps.

The data doesn't have to live in a spreadsheet. Playwright and pytest both let a team keep the cases in a table inside the test file, and plenty of teams prefer that for small sets. CSV or JSON files come in once the dataset grows or someone outside the team needs to edit it.

Reusing the logic doesn't mean every row follows the same path. A rejected input often ends on an error message instead of a confirmation page. A test that needs a stack of if-statements to handle every row is really two tests, and splitting them keeps both readable.

How Data-Driven Testing Works

Every data-driven run follows the same loop, no matter which tool sits underneath. The test checks its data and maps each row to the right steps before the app ever loads. Each case then starts from a clean state, and its assertions compare what happened with what the row promised.

Component Role
Dataset Supplies the inputs and the expected outcome for each case
Reader or provider Hands those values to the test
Mapping Connects each field to a parameter or a test step
Reusable logic Walks through the workflow under test
Assertions Compares actual behavior with the expected result
Setup and cleanup Keep one case from leaking into the next
Reporting Names each case and keeps the evidence when one fails

However, a spreadsheet full of cases doesn't test anything until the test reads those values and checks the outcome each row describes. Plenty of suites type every input from the file and then assert nothing beyond a page load, which leaves the real rule unchecked behind a green result.

Data-Driven Testing Example: Shipping Eligibility and Fees

Let’s see how data-driven testing works practically with an example:

Shipping quotes cover US and Canadian destinations only. Standard shipping costs $5 for merchandise subtotals under $100 and nothing at $100 or above, while express shipping always costs $15.

The store rejects negative subtotals and any destination outside those two countries. Amounts are in USD before tax, and the test doesn't validate street addresses. Every expected result in the table comes straight from that rule, never from the app.

Case ID Destination Method Subtotal (USD) Expected status Expected fee (USD) Coverage purpose
DDT‑01 US Standard 50.00 Accepted 5.00 Typical valid case
DDT‑02 US Standard 99.99 Accepted 5.00 Just below the threshold
DDT‑03 US Standard 100.00 Accepted 0.00 Exact threshold
DDT‑04 CA Standard 100.01 Accepted 0.00 Above the threshold, second supported country
DDT‑05 US Express 100.00 Accepted 15.00 Express ignores free standard shipping
DDT‑06 CA Standard -0.01 Rejected Not applicable Invalid subtotal
DDT‑07 GB Standard 100.00 Rejected Not applicable Unsupported destination

Each column has a clear job in the test. The three input columns fill in the quote form, while the two expected columns turn into assertions. Case ID and coverage purpose never touch the app, but they make a failure readable at a glance.

The assertions come in two kinds. An accepted row passes only when the quote shows the exact fee from the table, so a $5.00 charge on DDT-03 counts as a failure even though the page loaded fine. A rejected row passes when the app refuses the input, which means DDT-06 succeeds precisely because the store says no.

Every row serves a purpose: DDT-02 through DDT-04 sit on both sides of the $100 line, where off-by-one bugs like to hide. DDT-05 confirms that express doesn't inherit the free-shipping rule, and DDT-07 checks that the store turns away a country it doesn't serve.

How to Design Useful Test Datasets

A good dataset is intentionally short. Fifty rows of ordinary $40 carts test the same thing fifty times and slow down every run. The skill is picking rows that each tell the team something new about the rule, the core of good test case design.

Pick representative input classes.

Inputs fall into groups the app should treat the same way. Any standard US order under $100 belongs to one group, so one or two rows can stand in for all of them. Invalid inputs get their own groups, and so do business categories like destination or shipping method.

Test the boundaries on purpose.

Bugs cluster at edges because that's where someone typed < instead of <=. The shipping rule has its edge at $100, so the dataset checks $99.99 and $100.00 alongside $100.01. A value like -0.01 covers the bottom of the valid range, and the right boundary values always depend on the app's actual precision.

Cover the interactions that matter.

The three input fields can affect each other, and some bugs appear only when two of them meet. Testing every combination grows fast, so many teams turn to pairwise testing, which covers every pair of values at least once. NIST's combinatorial testing research points out the catch: some faults need three or more conditions at once, and pairwise sets can miss them.

Get expected results from a trusted source.

Get expected values from the reviewed business rule or its owner. Calculating them with the same code under test is tempting and quietly useless, since the test repeats whatever mistake the code makes. A $5 fee on a $100.00 order would pass every time.

Keep generated data reproducible.

Random data adds variety, and it sometimes finds inputs nobody thought to write down. It also produces failures that vanish on the next run unless the test saves the generated values and the seed. Generated rows work best next to the deliberate boundary cases, not in place of them.

A reviewer can judge a proposed row with a few quick questions:

  • Does it belong to an input class the dataset doesn't cover yet?
  • Is it sitting on or right next to a boundary?
  • Could this combination of conditions plausibly break?
  • Did its expected result come from the rule, not the app?
  • Would anyone notice if it disappeared tomorrow?

How to Set Up Data-Driven Testing?

Data-driven testing should follow a fixed setup order. Teams that start with test code and bolt on the data later usually end up rewriting both. The sequence below keeps the dataset and the logic in step from day one.

  1. A workflow worth reusing, where the steps stay stable, and only the values change
  2. A dataset schema that names every column and its type
  3. Stable case IDs that never get renumbered when rows move around
  4. Validation that stops a broken file before any test starts
  5. A mapping from columns to steps and to assertions
  6. A clean, independent starting state for every case
  7. Assertions that match each row's expected outcome
  8. Results recorded per case, with the dataset versioned next to the test

Validation gets less attention than it deserves. The check confirms that required columns exist and that every case ID is unique. It also confirms that numbers parse as numbers and that a blank cell means one specific thing.

The tricky part is that a valid dataset can intentionally hold invalid inputs. DDT-06's subtotal of -0.01 is a perfectly good test record, while the word "ninety" in that same column is a broken one. The schema has to accept the first and reject the second before the run begins.

CSV files bring a few small traps of their own. Spreadsheet apps strip leading zeros from ZIP codes and account numbers unless the column stays text, and a comma inside a quoted field can split one value into two. Date formats and file encoding need a written rule too, and production credentials should never be in the file.

Data-Driven Testing Tools and Automation Options

The tools below do different jobs, which is why head-to-head lists that rank Selenium against Cucumber rarely help anyone. Selenium controls a browser but leaves the data handling to a test runner like TestNG, which supplies cases through DataProviders. Every option still leaves work for the team, and that last column matters more than any feature list.

Option What it does Good fit What the team still owns
Selenium with TestNG Browser control plus a Java runner with DataProviders Java teams that want browser tests in code Data loading, mapping, assertions, setup, reporting, upkeep
Playwright Test Web testing with parameterized cases Teams that want browser and API checks in code Dataset design, fixtures, assertions, run configuration
pytest Python test runner with parametrize Unit and service tests in Python App interaction, fixtures, expected outcomes
Cucumber Scenario Outlines One readable scenario repeated over an Examples table Teams already practicing BDD Step code, data setup, runs, reporting
Functionize Studio Agent native testing with CSV-driven runs Testers who want plain-language tests without script upkeep Case selection, expected behavior, review, the release call

Here's the shipping dataset as a Playwright test, with the cases kept right in the file. Each row carries its ID into the test title, so a failure report names the exact case. Accepted and rejected rows live in separate loops, which keeps both sets of assertions short and honest.

TypeScript — Playwright Data-Driven Testing Example
import { test, expect, type Page } from '@playwright/test';

type Case = { id: string; dest: string; method: string; subtotal: string };
type AcceptedCase = Case & { fee: string };

const accepted: AcceptedCase[] = [
  { id: 'DDT-01', dest: 'US', method: 'Standard', subtotal: '50.00',  fee: '$5.00' },
  { id: 'DDT-02', dest: 'US', method: 'Standard', subtotal: '99.99',  fee: '$5.00' },
  { id: 'DDT-03', dest: 'US', method: 'Standard', subtotal: '100.00', fee: '$0.00' },
  { id: 'DDT-04', dest: 'CA', method: 'Standard', subtotal: '100.01', fee: '$0.00' },
  { id: 'DDT-05', dest: 'US', method: 'Express',  subtotal: '100.00', fee: '$15.00' },
];

const rejected: Case[] = [
  { id: 'DDT-06', dest: 'CA', method: 'Standard', subtotal: '-0.01' },
  { id: 'DDT-07', dest: 'GB', method: 'Standard', subtotal: '100.00' },
];

async function requestQuote(page: Page, c: Case) {
  await page.goto('/shipping-quote');
  await page.getByLabel('Country code').fill(c.dest);
  await page.getByLabel('Method').selectOption(c.method);
  await page.getByLabel('Subtotal').fill(c.subtotal);
  await page.getByRole('button', { name: 'Get quote' }).click();
}

for (const c of accepted) {
  test(`${c.id}: ${c.dest} ${c.method} at ${c.subtotal} costs ${c.fee}`, async ({ page }) => {
    await requestQuote(page, c);
    await expect(page.getByTestId('shipping-fee')).toHaveText(c.fee);
  });
}

for (const c of rejected) {
  test(`${c.id}: ${c.dest} ${c.method} at ${c.subtotal} is rejected`, async ({ page }) => {
    await requestQuote(page, c);
    await expect(page.getByRole('alert')).toBeVisible();
    await expect(page.getByTestId('shipping-fee')).toHaveCount(0);
  });
}
Example: Parameterized Playwright tests using accepted and rejected datasets

Two lines in that file carry most of the value. The fee assertion checks the exact amount, and the rejected loop checks that no fee shows up at all. Base URLs and browser projects belong in the Playwright config, which the official docs cover well.

Data-Driven vs. Keyword-Driven Testing, BDD, and Property-Based Testing

Data-driven testing separates values from logic, and keyword-driven testing separates actions from the code behind them. Behavior-driven development focuses on agreeing on behavior before anyone writes a test.

Approach Main concern Relationship to data-driven testing
Data-driven testing Varying inputs and expected outcomes while the logic stays the same The core technique
Keyword-driven testing Representing actions as reusable keywords Keyword tests can run against a dataset too
BDD Building shared understanding through concrete examples Scenario Outlines add data-driven runs
Property-based testing Generating inputs and checking a rule across all of them Complements hand-picked example rows

Keyword-driven testing isn't tied to spreadsheets, and it isn't less flexible. A keyword like "Request quote" can run against the same seven shipping rows as any coded test. Cucumber shows the BDD overlap just as clearly, since a Scenario Outline repeats one scenario for every row in its Examples table.

The terms "data-driven" and "parameterized" overlap even more. Some teams save "data-driven" for data in external files and "parameterized" for in-code tables, but that line isn't standard anywhere. Property-based tools like Hypothesis sit alongside both, generating hundreds of inputs to test a rule like "the fee is never negative."

Keeping Data-Driven Runs Reliable and Reports Readable

Most flaky data-driven suites aren't really flaky. Their cases share state, so one row's leftover cart or account changes what the next row sees. Each case needs its own setup and cleanup, and parallel cases need separate accounts or records.

When the dataset sits in the same repository as the test, any failure traces back to the exact rows that ran. A report that keeps skipped and blocked cases visible and separates first failures from retry results tells the real story of a run.

Fewer scripts don't mean fewer runs, and that part catches teams off guard. Forty rows across three browsers and two environments add up to 240 case runs when every combination goes. The cost shows up in pipeline time and cloud minutes even though the test file stayed small.

A pricing rule with hundreds of combinations finishes in seconds at the unit or API level. End-to-end tests then cover a handful of rows that prove the mapping and the user-facing behavior, and browser coverage follows real compatibility risk instead of habit. The regression testing guide covers how those layers fit into a release cycle.

Every case result is easier to trust when it carries the same core fields:

Field Why it matters
Case ID Points straight to the row that failed
Dataset version Shows which rows actually ran
Build and environment Separates code changes from config changes
Expected and actual result Makes the gap visible without a rerun
Failure category Splits app defects from test or data problems
Artifacts Screenshots, logs, or traces for the failed case

Common Data-Driven Testing Failures and How to Investigate Them

A failed row comes from one of two places: the app, or the test and its data. The symptom usually points to which one, long before anyone opens a debugger. The patterns below recur.

Symptom Likely cause Next step
Every case uses the same value Wrong mapping or a hardcoded input Log inputs for each case, compared side by side
A leading-zero ID loses its zero Numeric conversion on import Column kept as text
One row fails only after another Shared state or missed cleanup Separate setup and records per case
Fee is wrong at exactly $100 A real business-rule defect Expected value kept, bug filed
Run fails before touching the app Parsing or schema error A fix to the dataset, not the app
Every row passes despite wrong fees Missing or weak assertions Assertions on the exact fee per case
A rejected case has no result Loop stopped early Remaining cases reported as not run, control flow reviewed
A failure won't reproduce Generated values weren't saved Inputs and seed captured on every run

The $100 row deserves the most care. When DDT-03 comes back with a $5.00 fee, the temptation is to change the expected value and move on, especially close to a release. T

Malformed records call for the opposite response. A word sitting in the subtotal column is the dataset's fault, and the fix belongs in the file. The negative subtotal in DDT-06 is a deliberate test, so a failure there points straight at the app.

Data-Driven Testing With Functionize Studio

Everything above assumes someone writes and maintains the test code. Functionize Functionize Studio takes a different route: the tester describes the workflow in plain language, and Studio builds the test and keeps it working as the UI changes. For the shipping example, that description reads almost like the business rule, naming the quote form and the fee to check.

The dataset arrives as a CSV. After the tester attaches the file and maps its columns to the test steps, Studio runs the test once for each row. Those runs run across browsers in parallel, so the seven shipping cases don't wait in line.

Accepted and rejected rows check different things, and the cleanest setup in Studio mirrors the code version. One test covers the five accepted rows and checks the fee, while a second covers the two rejected rows and checks the error. Neither test needs branching logic, and each stays easy to read.

The demo plants one bug, where DDT-03 charges $5.00 at a $100.00 subtotal. A pass is a red result for that row, with Studio's diagnosis separating a real regression from a flaky locator or a slow page. The case ID should then trace that failure straight back to the $100 boundary. A green run means you are good to go, and a red one is worth your attention.

Studio takes over the authoring and the upkeep, but the standard is yours. The team still decides which rows belong in the dataset and what each one should return. The release call stays with people, now backed by evidence from every case, which is what testing you direct looks like day to day.

The Free plan gives enough room to build something real, and a seven-row dataset makes a sensible first project. The Agent Tasks page shows what else Studio can handle once that first test runs green. Sign-up is self-serve, so the tester can run the whole trial on their own schedule.

How to Evaluate a Data-Driven Testing Tool

A demo with a five-row login table proves very little. The useful test is a pilot built on one real workflow from your own app, with boundary and rejected rows sitting next to the ordinary ones. Two planted problems make the pilot honest: one real defect in the app and one malformed record in the file.

A good tool clearly separates those two problems. A weak one reports both as the same red failure and leaves the tester to sort it out by hand. Beyond that pilot, a short list of questions quickly sorts the options.

Area What to check
Data sources Which formats import directly, and how types and leading zeros survive the import
Mapping How columns reach both the steps and the expected-result checks
Outcomes How accepted and rejected rows get different assertions
Isolation Setup, cleanup, and safety when rows run in parallel
Reporting Case IDs in results, single-row reruns, saved inputs
Data handling How sensitive test data and private app access are protected
Cost Run limits, and what 240 case runs cost per build
Coexistence How the tool sits beside the suites already in place

The build is the easy part, and with a code-based setup the maintenance has no end date. A platform moves that upkeep off the team, and it pays off only when the tester still owns the dataset and the release call.

Frequently Asked Questions

What Is Data-Driven Testing?

Data-driven testing is running one set of test steps against many inputs, each with its own expected result. The data lives in a table, either inside the test file or outside it. For example, a shipping-fee test can check seven price and destination cases without seven copies of the steps.

Does Data-Driven Testing Need an External Spreadsheet?

Data-driven testing doesn’t need an external spreadsheet. Playwright, pytest, and TestNG all support cases defined right in the code. External files start to make sense once the dataset grows or people outside the dev team need to edit it.

How Is Data-Driven Testing Different From Keyword-Driven Testing?

Data-driven testing separates values from test logic. Keyword-driven testing separates reusable actions from the code that performs them. The two work well together, since a keyword test can run against a dataset just like a coded one.

Can Data-Driven Testing Work With BDD?

Yes, data-driven testing works with BDD, and Cucumber makes it easy. A Scenario Outline runs one scenario for every row in its Examples table. The result is a business-readable scenario with data-driven coverage underneath.

How Should Negative Test Cases Appear in a Dataset?

Each one gets its own row with the invalid input and an expected status of "Rejected." The test passes when the app refuses that input. A broken record, like text in a number column, is a separate problem that dataset validation catches before the run.

How Do You Keep a Large Dataset From Slowing Every Run?

Most combinations belong at the unit or API level, where they finish in seconds. End-to-end tests then cover the few rows that prove the user-facing behavior. Browser coverage follows real compatibility risk rather than running every row everywhere.

About the author

author photo: Tamas Cser

Tamas Cser

Founder & CEO

Tamas Cser is the founder, CEO, and Chief Evangelist at Functionize, the leading provider of AI-powered test automation. With over 15 years in the software industry, he launched Functionize after experiencing the painstaking bottlenecks with software testing at his previous consulting company. Tamas is a former child violin prodigy turned AI-powered software testing guru. He grew up under a communist regime in Hungary, and after studying the violin at the University for Music and Performing Arts in Vienna, toured the world playing violin. He was bitten by the tech bug and decided to shift his talents to coding, eventually starting a consulting company before Functionize. Tamas and his family live in the San Francisco Bay Area.

Author linkedin profile