You wrote a login test. It passes. Then a teammate asks the obvious question: does it pass for the locked account, the unverified email, the password with a trailing space, the SQL-injection string someone will eventually paste into the field? Now you have a choice. Copy that test five times and change one value in each copy, or find a way to feed the same test many rows of input and let it run them all.
The copy-paste route is how most test suites rot. Five near-identical tests drift apart over a year. One gets a new assertion, the others don’t. A field rename breaks four of them silently. You end up maintaining five things that should have been one. Parameterized testing fixes this at the root: you write the test once, then point it at a table of inputs and expected outputs. One scenario, hundreds of cases, a single place to edit.
What parameterized testing actually means
Parameterized testing, sometimes called data-driven testing, separates the logic of a test from the data it runs against. The logic is the sequence of steps: send a request, check the status code, validate a field in the response. The data is the set of inputs and expectations you want that logic to run against.
Picture a single test scenario for a discount-code endpoint. The logic is always the same. Send POST /api/orders with a code, then assert on the response. The data changes per case:
| code | order_total | expected_status | expected_discount |
|---|---|---|---|
| WELCOME10 | 100 | 200 | 10 |
| WELCOME10 | 5 | 422 | 0 |
| EXPIRED | 100 | 410 | 0 |
| (empty) | 100 | 400 | 0 |
| FAKE123 | 100 | 404 | 0 |
Five rows, five distinct behaviors, one test. The runner iterates over the rows. On each pass it binds the column values to variables, fires the request, and checks the assertions against that row’s expectations. When row 3 fails because the expired code returned 200 instead of 410, you get one clear failure pointing at one row. You don’t go hunting through five separate test files to figure out which copy broke.
This pattern matters most at the edges. Happy-path coverage is easy to write and rarely catches the bug that pages you at 2 a.m. The bugs live in the boundary cases: the empty string, the negative number, the unicode name, the expired token, the value that’s one cent over the limit. Parameterization makes adding a boundary case as cheap as adding a row to a spreadsheet.
Why a separate data file beats hardcoded values
You could hardcode each case directly in the test. Most people start there. The problem shows up later.
When the data lives in the test, a non-engineer can’t contribute cases. Your QA lead knows fifteen tricky inputs that have broken this endpoint before, but they can’t add them without editing code and opening a pull request. When the data lives in a CSV, they edit a spreadsheet and commit it. The barrier drops to near zero.
A separate file also keeps your test scenario readable. A test that loops over an external file is short: one request, a handful of assertions, done. A test with thirty inline cases is a wall of repetition that nobody wants to touch. And when you need to generate cases programmatically, say a thousand rows pulled from production logs, a file is the only sane option. You can’t paste a thousand cases into a test body.
The format you pick depends on the shape of your data. Flat, tabular cases fit CSV. Nested or structured payloads fit JSON. Both are first-class inputs in Apidog’s runner, so the choice is about your data, not about tool limitations.
Setting up your data file
Start with CSV for tabular cases. The header row names your variables; every row below is one iteration. Here’s the discount-code table as a real file, discount-cases.csv:
code,order_total,expected_status,expected_discount
WELCOME10,100,200,10
WELCOME10,5,422,0
EXPIRED,100,410,0
,100,400,0
FAKE123,100,404,0
Each column header becomes a variable you reference inside the test. In the request body you write {{code}} and {{order_total}}; in the assertions you compare against {{expected_status}} and {{expected_discount}}. The runner does the binding row by row.
When your inputs are nested, reach for JSON. An array of objects, one object per iteration, lets each case carry structured data that would be awkward to flatten into columns. Here’s user-cases.json for a user-creation endpoint where the payload has nested fields:
[
{
"scenario": "valid full profile",
"user": {
"email": "ada@example.com",
"roles": ["admin", "billing"],
"profile": { "country": "US", "timezone": "America/New_York" }
},
"expected_status": 201
},
{
"scenario": "missing email",
"user": {
"email": "",
"roles": ["viewer"],
"profile": { "country": "GB", "timezone": "Europe/London" }
},
"expected_status": 400
},
{
"scenario": "unknown role",
"user": {
"email": "grace@example.com",
"roles": ["wizard"],
"profile": { "country": "CA", "timezone": "America/Toronto" }
},
"expected_status": 422
}
]
Inside the test you reference the structured values with the same {{user}}, {{expected_status}} syntax, and Apidog hands each object’s fields to the iteration. The scenario column is a label for yourself; it shows up in the report so a failed iteration reads “unknown role” instead of “iteration 3.”
A few rules keep data files from biting you:
- Keep one concern per file. A discount-code file and a user-creation file are two files, not one with mixed columns.
- Put the expected result in the data, not the test. The whole point is that row 2 expects a 422 while row 1 expects a 200. If the expectation is hardcoded, you’re back to one test per case.
- Quote anything with a comma in CSV, or switch that file to JSON. A free-text field with commas in it is the classic CSV footgun.
- Store these files in your repo next to the rest of your test config so they version alongside the code they exercise.
Building the parameterized scenario in Apidog
In the Apidog app, build the test scenario once like any other. Add the request to your endpoint. In the body, swap the literal values for variable references: {{code}}, {{order_total}}, and so on. These are the columns from your data file.
Then add the assertions that read from the same file. For the discount example you’d assert that the response status equals {{expected_status}} and that the discount field in the JSON body equals {{expected_discount}}. Because both the input and the expected output come from the row, the same assertion logic validates every case correctly. If you haven’t written assertions in Apidog before, API assertions: a practical guide covers the patterns, and how to set assertions and extract variables from a JSON response shows the JSONPath side in detail.
To wire in the data, open the test scenario’s run settings and attach your CSV or JSON file as the iteration data source. Apidog reads the file, counts the rows, and runs the scenario once per row, binding each row’s columns to the matching variables. Run it inside the app and you get a per-iteration breakdown: which rows passed, which failed, and the actual versus expected value for each failed assertion.
This is also where parameterization composes with the rest of your suite. A parameterized scenario is still a scenario, so you can group several of them into a test suite and run the whole set as one job. The data-driven loop handles breadth within a single endpoint; the test suite handles coverage across endpoints.
Running it from the command line
The app is where you build and debug. CI is where the test earns its keep, running on every pull request without anyone clicking a button. That handoff is what the Apidog CLI is for. It takes the scenario you built in the app and runs it headlessly from a terminal, with the same iteration data.
The CLI ships as an npm package. Install it globally:
npm install -g apidog-cli
The binary is apidog, so every command starts with apidog run. A basic run points at a scenario by ID and an environment by ID:
apidog run --access-token $APIDOG_ACCESS_TOKEN -t 605067 -e 1629989 -r cli
To drive that scenario from a data file, add the iteration-data flag. It accepts a path to a JSON or CSV file:
apidog run --access-token $APIDOG_ACCESS_TOKEN -t 605067 -e 1629989 \
-d ./discount-cases.csv -r cli,junit --out-dir ./test-reports
The -d flag (long form --iteration-data) is the heart of parameterized runs from the command line. Apidog reads the file, runs the scenario once per row, and reports each iteration. Swap discount-cases.csv for user-cases.json and the same flag handles the JSON array; the runner picks up the format from the file. Treat the access token like a password and store it as a CI secret, never in a committed file. That’s why every example references $APIDOG_ACCESS_TOKEN rather than a literal value.
A few flags pair naturally with parameterized runs:
-d, --iteration-data <path>points the run at your CSV or JSON file. This is the one that turns a single-pass run into a data-driven one.-n, --iteration-count <n>runs the scenario a fixed number of times. When you supply a data file, the row count usually drives the iterations, so you reach for-nmainly for repeat-without-data cases like soak tests.-r, --reporters <list>chooses output formats. Useclifor readable build-log output andjunitto emit the XML that CI dashboards parse into a pass/fail tree per iteration.--out-dir <path>sets where reports land so you can archive them as build artifacts.
If you want the authoritative, current list of flags for your installed version, run apidog run --help. The CLI prints every option with its short and long form.
Wiring parameterized runs into CI
The reason to invest in parameterized tests is that they pay off automatically. Here’s a GitHub Actions job that installs the CLI and runs a CSV-driven scenario on every pull request:
name: API tests
on: [pull_request]
jobs:
api-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Apidog CLI
run: npm install -g apidog-cli
- name: Run parameterized API tests
env:
APIDOG_ACCESS_TOKEN: ${{ secrets.APIDOG_ACCESS_TOKEN }}
run: |
apidog run --access-token $APIDOG_ACCESS_TOKEN \
-t 605067 -e 1629989 \
-d ./tests/discount-cases.csv \
-r cli,junit --out-dir ./test-reports
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: api-test-report
path: ./test-reports
The token comes from secrets.APIDOG_ACCESS_TOKEN, set once in your repo settings. The junit reporter writes XML that most CI dashboards turn into a per-iteration result tree, so a failing row shows up as a named failing test rather than a wall of log text. The if: always() on the upload step means you keep the report even when the run fails, which is exactly when you want it. For a deeper walkthrough of the Actions side, see how to automate API tests in GitHub Actions.
The same scenario and the same data file run in any CI system. GitLab CI, Jenkins, CircleCI, and the rest all reduce to the same three moves: install Node and the CLI, expose the token as an environment variable, call apidog run with -d. There’s no per-platform rewrite of your tests.
Comparing parameterized testing approaches
Apidog isn’t the only way to run data-driven API tests. Worth knowing the landscape so you pick the right fit.
Postman and its runners support data files too. With Postman’s Collection Runner or the Newman command-line tool, you attach a CSV or JSON file and reference {{column}} variables in requests, much like the pattern here. It’s a capable approach and well documented. The trade-off is that your test logic lives in JavaScript pre-request and test scripts, so as your assertions grow, you maintain more code. If you’re weighing the command-line runners specifically, Postman CLI vs Newman breaks down the differences.
Code-first frameworks like pytest with @pytest.mark.parametrize, JUnit’s @ParameterizedTest, or REST Assured give you full programming-language control. They’re the right call when your test logic genuinely needs code: complex setup, custom data generation, tight coupling to an existing test codebase. The cost is that every case lives in code, so non-engineers can’t contribute, and you maintain the HTTP plumbing yourself.
Apidog’s angle is that the scenario is visual and the data is external, so the logic stays readable and the cases stay open to anyone who can edit a spreadsheet, while the same scenario still runs headlessly in CI. If you’re specifically choosing a tool for CSV and JSON data-driven runs, the comparison in which tool for data-driven API testing with CSV or JSON goes deeper on the tradeoffs. None of these is wrong. Match the approach to who writes the cases and how much custom logic each case needs.
A practical workflow that scales
Here’s how this looks once it’s part of your team’s routine.
Start narrow. Pick one endpoint that’s bitten you before. Write the single scenario in Apidog with variable references in the request and the expected result in the assertions. Build a CSV with three rows: one happy path, one known failure, one boundary case. Run it in the app until all three iterations behave as expected.
Then grow the data, not the test. Every time a bug report comes in, add the input that caused it as a new row with its correct expected output. The bug becomes a permanent regression case and you never wrote a new test, you added a line to a file. Over a few months the file accumulates the real-world ugliness your endpoint actually faces.
Finally, automate it. Drop the apidog run -d command into CI so the whole table runs on every pull request. Now a change that breaks the expired-code path fails the build the moment it’s pushed, with a named iteration pointing straight at the broken row.
The compounding win is maintenance. When the endpoint’s response shape changes, you fix the assertion once and every case picks up the fix. When you need fifty more cases, you add fifty rows. The test logic stays a single, small, readable thing no matter how wide your coverage gets.
Frequently asked questions
What’s the difference between parameterized testing and data-driven testing? They describe the same idea and people use the terms interchangeably. Both mean running one test repeatedly with different inputs and expected outputs supplied from outside the test. “Parameterized” leans on the parameter-binding mechanism; “data-driven” leans on the external data source. In practice, treat them as synonyms.
Should I use CSV or JSON for my data file? Match the format to your data shape. Flat, tabular cases where every row has the same simple columns fit CSV, and CSV is easier for non-engineers to edit in a spreadsheet. Nested or structured payloads, like a request body with arrays and sub-objects, fit JSON. Apidog reads both as iteration data, so pick whichever represents your cases without contortion.
Will hundreds of iterations slow down my pipeline? Each row is one run of the scenario, so total time scales with row count times per-request latency. For most API tests that’s seconds, not minutes. If a large data set does stretch your build, split it: run a fast smoke subset on every pull request and the full table on a nightly or pre-release job. The same scenario and file power both; only the data subset changes.
How do I keep secrets out of my data files and test config? Keep credentials out of the data file entirely. Tokens and passwords belong in environment variables or your CI system’s secret store, referenced as $APIDOG_ACCESS_TOKEN and the like. The data file should hold test inputs and expected results, not authentication material. Anyone who can read the repo can read the CSV, so treat it that way.
Can I run the same parameterized scenario against staging and production? Yes. Keep the scenario and the data file fixed, and switch environments with the -e flag. Point a pull-request check at staging and a post-deploy smoke test at production using the same scenario ID and the same data, just a different environment ID. That’s the whole reason environment and data are separate inputs.
Wrapping up
Parameterized API testing turns coverage from a copy-paste chore into a data-entry task. You write the test once, describe each case as a row in a CSV or JSON file, and let the runner do the rest. The logic stays small and readable, the cases stay open to anyone on the team, and CI runs the whole table on every change.
Apidog gives you the visual scenario builder for authoring, external CSV and JSON files for the data, and the apidog run -d command for headless execution in any CI system. Build one scenario, point it at a growing table of cases, and your test coverage widens every time someone adds a row. Download Apidog and turn your next flaky one-off test into a parameterized scenario that scales.



