Your database isn’t just a place to store data; it’s an active participant in your application’s state, and your tests need to control that state.
Imagine a typical Cypress test suite. You have tests for user login, creating a new post, and then maybe editing that post. Without a controlled starting point, each test might depend on the previous one’s side effects. Test A creates a user, Test B uses that user, and Test C modifies the post created in Test B. If Test A fails, or if you run Test C in isolation, it breaks. This is where seeding your database comes in. It’s about establishing a known, consistent state before your tests begin to execute.
Here’s a simplified look at what that might look like in practice. Let’s say you’re using Node.js with a PostgreSQL database.
// cypress/support/e2e.js
// ... other imports
import { seedDatabase } from '../../scripts/db-seed'; // Assuming your seed script is here
beforeEach(() => {
// Before each test, ensure the database is clean and seeded
cy.exec('npm run db:reset', { failOnNonZeroExit: false }); // Optional: full reset
seedDatabase(); // Your custom seeding function
});
// ... rest of your support file
And your scripts/db-seed.js might look something like this:
// scripts/db-seed.js
const { Pool } = require('pg');
const pool = new Pool({
user: 'testuser',
host: 'localhost',
database: 'myapp_test',
password: 'password',
port: 5432,
});
async function seedDatabase() {
const client = await pool.connect();
try {
// Clear existing data (order matters for foreign keys)
await client.query('DELETE FROM posts;');
await client.query('DELETE FROM users;');
// Insert new data
const userResult = await client.query(
"INSERT INTO users (username, email) VALUES ($1, $2) RETURNING id",
['testuser', 'test@example.com']
);
const userId = userResult.rows[0].id;
await client.query(
"INSERT INTO posts (title, body, user_id) VALUES ($1, $2, $3)",
['My First Post', 'This is the body of the first post.', userId]
);
console.log('Database seeded successfully.');
} catch (err) {
console.error('Error seeding database:', err.stack);
} finally {
client.release();
}
}
// If running this script directly for setup
if (require.main === module) {
seedDatabase().then(() => process.exit(0)).catch(() => process.exit(1));
}
module.exports = { seedDatabase };
The core problem this solves is test flakiness and non-determinism. When tests depend on each other’s side effects or the state left over from a previous run, they become brittle. A seeding script, executed before your test suite (or before each test, depending on your needs), guarantees that every test starts with the exact same, predictable data. This means if a test fails, you know it’s a genuine application bug, not an environmental or state-related issue.
Internally, this process involves a script that connects to your test database and executes SQL commands. You’ll typically want to:
- Clear existing data: This is crucial for a clean slate. You’ll use
DELETE FROM your_table;statements. The order is important here; you usually delete from tables with foreign key constraints first (e.g., deletepostsbeforeusersifpostshas auser_idforeign key). - Insert seed data: Use
INSERT INTO your_table (column1, column2) VALUES (value1, value2);to populate the database with the specific data your tests require. This might include users, products, configurations, etc. - Handle relationships: If you have foreign keys, ensure you insert parent records first and then use their generated IDs when inserting child records. The
RETURNING idclause in PostgreSQL (or similar in other databases) is invaluable for this.
The exact commands and structure will vary based on your database (MySQL, SQLite, MongoDB, etc.) and your ORM or query builder (Knex, Prisma, TypeORM, Mongoose, or raw SQL). For example, with MongoDB and Mongoose, you might use MyModel.deleteMany({}) and MyModel.create([...]).
The most surprising part is how often developers overlook the order of deletions. If you have a users table and an orders table where orders has a user_id foreign key pointing to users, attempting to DELETE FROM users; first will fail if there are any orders referencing those users. The database constraint will prevent it. You must delete from the "many" side of the relationship (orders) before deleting from the "one" side (users).
After seeding your database, the next challenge is often efficiently updating specific pieces of data within a test without re-seeding the entire database.