Skip to main content

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

ScenarioEdge caseWhy tests breakApproach
Bad rowLine 47 invalidPartial import?Probe error report + count
Missing headerTemplate v2All rows failFixture missing-column.csv
Export filterExport all vs viewWrong rowsParse CSV assert ids
Download eventLate listenerLost filewaitForEvent before click
Large fileTimeoutCI failSmaller fixture + probe batch
Duplicate keysSecond importUpsert logicProbe unique count
Empty CSVZero rowsCrashEmpty fixture spec
Formula injection=cmd cellSecuritySanitize probe
Timezone datesISO vs localWrong dayAssert seeded iso string
import_schema_versionBreaking changeSilent dropVersion 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

ArrangeActAssert
100 rows, filter status=active (10 visible)ExportParsed CSV length 10
Select 3 checkboxesExport selectedCSV 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-patternWhy it failsBetter approach
Assert import toastRows not committedProbe counts
Export without filter seedNondeterministic lengthSeed + filter UI
Parse CSV before download completeTruncated fileawait download.saveAs
Skip bad-row specProd data corruptbad-type fixture
Manual copy-paste CSV in UINot reproduciblesetInputFiles

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.

  1. Arrange: Fixture bad-type-row-47.csv; seed empty user table for runId.
  2. Act: Upload and confirm import.
  3. 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).

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.

Start free on TestChimp · Book a demo