Fixtures are only one part of a complete test data strategy. This lesson compares static fixtures, dynamically generated data, and backend seeding approaches for keeping tests both realistic and independent.
Static vs. Dynamic Test Data
Static fixtures are simple and predictable but can become stale or collide when multiple tests need slightly different variations of similar data. Dynamically generated data, built at runtime with a factory function, trades a little predictability for flexibility and avoids hardcoded collisions like duplicate emails across parallel test runs.
Using Date.now() (or a UUID) in generated data avoids collisions when many tests run in parallel against a shared environment.
Common Test Data Sources
cy.fixture('data.json') // static file
createUser({ role: 'admin' }) // dynamic factory function
cy.request('POST', '/test/seed') // backend seeding via API
cy.task('db:seed', { ... }) // backend seeding via Node task
Static fixtures are best for stable, rarely-changing reference data.
Factory functions are best for data that needs slight variation per test or run.
API-based seeding sets up real backend state for tests that depend on it existing.
cy.task() runs Node.js code directly, useful for direct database access during seeding.
Test Data Strategy Cheat Sheet
Choosing the right test data approach for a given scenario.
Scenario
Recommended Approach
Stable reference data (e.g., country list)
Static fixture
Data needing uniqueness per run
Dynamic factory function
Backend state must exist before a test
API seeding via cy.request()
Direct DB access needed for setup
cy.task() calling Node.js code
Mocking a response shape only, no real backend
Static fixture with cy.intercept()
Seeding Backend State via API
When a test needs real backend state to exist, an account, an order, a specific permission, calling a dedicated setup API endpoint directly (rather than clicking through the UI to create it) is significantly faster and decouples the test's setup from unrelated UI flows.
This test-only seeding endpoint should be disabled outside test/staging environments for safety.
Using cy.task() for Direct Data Access
cy.task() runs arbitrary Node.js code in Cypress's backend process, giving tests access to things a browser cannot do directly, like querying a database or reading a file on disk, useful when no convenient API endpoint exists for seeding or verification.
// cypress.config.js
export default defineConfig({
e2e: {
setupNodeEvents(on) {
on('task', {
'db:seed'(data) {
return seedDatabase(data); // your own Node.js logic
},
});
},
},
});
// in a spec file
cy.task('db:seed', { users: 3 });
Common Mistakes
Hardcoding unique-looking values (like a specific email) that eventually collide across parallel or repeated test runs.
Setting up backend state by clicking through the UI when a direct API call would be far faster and more reliable.
Leaving test-only seeding endpoints reachable in production environments.
Mixing static fixtures and dynamic factories inconsistently for the same kind of data across a suite.
API-based seeding sets up real backend state faster and more reliably than UI-driven setup.
cy.task() gives tests access to Node.js-level operations like direct database seeding.
Always guard test-only setup/reset endpoints so they cannot run in production.
Pro Tip
Default to API-based seeding for anything beyond the specific feature under test, if a test needs a logged-in user with existing orders to test refunds, create that user and those orders via API calls, and reserve UI-driven steps for the actual behavior being verified.
You now have a broader test data strategy toolkit. Next, learn how to manage per-environment values with Environment Variables.