How to Test CSV Import and Export
Short answer
CSV import/export corrupts production data silently when headers drift, bad rows partially commit, or exports ignore grid filters. Test with fixture CSVs in repo, setInputFiles for import, waitForEvent('download') + parse for export, and probe Assert on row counts and error reports—not grid row text alone.
Part of Testing Guides by integrations.
Who this is for
Admin tools, CRM imports, billing migrations, and reporting UIs with CSV upload wizards and "Export to CSV" on data grids.
Why testing CSV matters
- Data corruption — partial import commits 999 of 1000 rows
- Wrong exports — "Export" dumps all rows while UI filter active
- Encoding bugs — UTF-8 BOM breaks downstream Excel
- Schema drift — new column in template untested
Complexity map
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| Bad row | Line 47 invalid | Partial import? | Probe error report + count |
| Missing header | Template v2 | All rows fail | Fixture missing-column.csv |
| Export filter | Export all vs view | Wrong rows | Parse CSV assert ids |
| Download event | Late listener | Lost file | waitForEvent before click |
| Large file | Timeout | CI fail | Smaller fixture + probe batch |
| Duplicate keys | Second import | Upsert logic | Probe unique count |
| Empty CSV | Zero rows | Crash | Empty fixture spec |
| Formula injection | =cmd cell | Security | Sanitize probe |
| Timezone dates | ISO vs local | Wrong day | Assert seeded iso string |
| import_schema_version | Breaking change | Silent drop | Version header row test |
Import: fixture files
fixtures/csv/
valid-10-rows.csv
missing-header.csv
bad-type-row-47.csv
duplicate-keys.csv
empty.csv
await page.goto('/admin/import');
await page.getByLabel('CSV file').setInputFiles(
path.join(fixtures, 'valid-10-rows.csv'),
);
await page.getByRole('button', { name: /import/i }).click();
await expect.poll(async () => {
const res = await request.get(`/api/test/probe-import?runId=${runId}`);
return (await res.json());
}).toMatchObject({ imported: 10, failed: 0 });
See file uploads for setInputFiles patterns.
Import: row-level errors
await page.getByLabel('CSV file').setInputFiles(
path.join(fixtures, 'bad-type-row-47.csv'),
);
await page.getByRole('button', { name: /import/i }).click();
await expect(page.getByText(/row 47|line 47/i)).toBeVisible();
await expect.poll(() => probeImport(runId)).toMatchObject({
imported: 46,
failed: 1,
});
Assert probe defines product policy: all-or-nothing vs partial.
Export: download + parse
From Playwright downloads:
import { parse } from 'csv-parse/sync';
// Seed grid data via API first
await seedGridRows(runId, [{ id: 'a1' }, { id: 'a2' }, { id: 'b1' }]);
await page.goto('/admin/users?status=active');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: /export csv/i }).click();
const download = await downloadPromise;
const filePath = path.join(test.info().outputDir, 'export.csv');
await download.saveAs(filePath);
const content = fs.readFileSync(filePath, 'utf-8');
const records = parse(content, { columns: true });
expect(records).toHaveLength(2); // filtered active only
expect(records.map(r => r.id).sort()).toEqual(['a1', 'a2']);
Register download listener before click.
Export matches grid filter
| Arrange | Act | Assert |
|---|---|---|
| 100 rows, filter status=active (10 visible) | Export | Parsed CSV length 10 |
| Select 3 checkboxes | Export selected | CSV ids match selection |
Link data grids guide for filter setup.
Encoding and BOM
If Excel users require BOM:
expect(content.charCodeAt(0)).toBe(0xfeff); // optional BOM assert
expect(content).toContain('résumé'); // UTF-8 round trip
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
| Assert import toast | Rows not committed | Probe counts |
| Export without filter seed | Nondeterministic length | Seed + filter UI |
| Parse CSV before download complete | Truncated file | await download.saveAs |
| Skip bad-row spec | Prod data corrupt | bad-type fixture |
| Manual copy-paste CSV in UI | Not reproducible | setInputFiles |
Example scenario
Situation: Admin imports user CSV with one invalid email on row 47.
Expected outcome: Error report cites row 47; probe shows 46 imported or zero per policy—never silent drop.
Why UI-only automation breaks: UI shows success while probe imported 46 without surfacing row 47 failure.
- Arrange: Fixture bad-type-row-47.csv; seed empty user table for runId.
- Act: Upload and confirm import.
- Assert: Probe import counts and error row 47; UI error optional.
TestChimp workflow: Track import_schema_version when template column added without test.
Same Arrange/Act/Assert pattern as expired-coupon checkout.
Connect scenarios to your QA workflow
Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).
Related scenarios
- File uploads — binary upload mechanics
- Data grids — filters before export
- PDF downloads — download event pattern
External references
Frequently asked questions
How do I validate exported CSV in Playwright?
waitForEvent download before Export click, saveAs, parse with csv-parse. Assert row count and ids match seeded filtered data—not unfiltered DB.
How do I test import row errors?
Fixture CSV with known bad row. Assert UI error references row number and probe imported/failed counts match product policy.
Partial import vs all-or-nothing?
Document product behavior in scenario markdown. Probe is authoritative—assert exact imported and failed counts.
Export button does not trigger download event?
Some apps open blob URL—use page.request.get on export API or wait for response. Prefer direct API assert when UI is not under test.
How do I test new CSV template columns?
Add fixture with new header and import_schema_version metadata. Probe mapped columns; TrueCoverage flags prod imports on new version without scenario.
Large CSV times out in CI?
Use smaller representative fixtures in PR job; probe batch job completion for large imports in separate job.
How do I link CSV scenarios to requirements?
Use // @Scenario: in SmartTests pointing to markdown import/export cases—rolls up in test planning folders.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.