When testing applications that depend on cloud services, manually preparing database records for every test can quickly become time-consuming, repetitive, and error-prone.
A better approach is to automate test-data creation using reusable fixtures, custom Cypress tasks, and the AWS SDK.
In this article, I'll walk through how I built a mocks-first testing approach using Cypress and AWS DynamoDB, where each test can create the data it needs, execute independently, and verify the resulting database state.
Why Mocks-First Testing?
A mocks-first approach allows tests to control their required data and dependencies instead of relying on manually prepared records.
Rather than creating database records before running a test, the test itself prepares the required state during execution.
This provides several benefits:
- Faster test execution
- Repeatable test scenarios
- Reusable test data
- Less manual setup
- Better test isolation
- Easier maintenance
- More reliable CI/CD execution
The goal is not to mock every AWS service. Instead, the approach focuses on controlling test data and external dependencies so that tests remain predictable and repeatable.
The Overall Approach
The architecture can be summarized as:
Cypress Test → cy.task() → Node.js → AWS SDK → DynamoDB
The Cypress test is responsible for describing the scenario and assertions, while the Node.js layer handles AWS-specific operations.
This separation keeps the test code clean and prevents AWS database logic from being duplicated across test cases.
Step 1: Create Reusable Mock Data
The first step is to create reusable JSON fixtures.
cypress/
└── fixtures/
└── test-data.json
The fixture contains the information required to create the database record.
Instead of hardcoding database values inside every test, the fixture becomes the base payload that can be reused across multiple scenarios.
For example:
{
"id": "test-12345",
"name": "Sample Test Data",
"category": "TEST",
"status": "ACTIVE"
}
Different fixtures can also be created for:
- Positive scenarios
- Negative scenarios
- Boundary conditions
- Error scenarios
- Different transaction states
This keeps test data separate from test logic.
Step 2: Separate Database Logic
The next step is to keep DynamoDB operations outside the Cypress test itself.
A dedicated helper can be responsible for:
- Creating the DynamoDB client
- Converting fixture data into database records
- Generating partition keys and sort keys
- Calculating TTL values
- Inserting records
- Retrieving records
- Deleting test data when required
For example, a helper might expose operations such as:
insertMockData()
getMockData()
deleteMockData()
The important principle is separation of concerns.
The test should describe what data it needs, while the helper handles how that data is stored in DynamoDB.
Step 3: Expose Database Operations Through Cypress Tasks
Cypress provides cy.task() as a bridge between the Cypress test environment and Node.js.
Instead of putting AWS SDK operations directly inside the browser-side test, the test can call a custom task:
cy.task('insertMockData', mockData);
The flow becomes:
Cypress Test
│
▼
cy.task()
│
▼
Node.js
│
▼
AWS SDK
│
▼
DynamoDB
Typical Cypress tasks can include:
insertMockDatagetMockDatadeleteMockData
This approach centralizes AWS operations and makes them reusable across the entire test suite.
Step 4: Configure Environment Variables
Cloud configuration should never be hardcoded inside test files.
Environment-specific values can be provided through configuration such as:
- AWS Region
- AWS credentials
- DynamoDB table name
- Local DynamoDB endpoint
- Other environment-specific settings
This allows the same test suite to run against different environments.
For example:
Local
↓
Development
↓
QA
↓
CI/CD
The test logic remains unchanged while the configuration changes according to the execution environment.
Credentials and secrets should always be managed securely and should never be committed to the repository.
Step 5: Build Flexible Test Data
Creating a separate fixture for every test scenario can eventually lead to duplicated test data.
Instead, a base fixture can be loaded and overridden only where necessary.
For example:
const testData = {
...baseFixture,
id: `test-${Date.now()}`,
status: 'INACTIVE'
};
The base fixture provides the common structure, while the test customizes only the values relevant to the scenario.
This approach makes it easier to generate unique test records while keeping the fixture maintainable.
It is especially useful when testing:
- Multiple transactions
- Different statuses
- Different merchants
- Edge cases
- Concurrent processing
- Large datasets
Step 6: Verify the Database State
Creating test data is only part of the process.
After the application processes the data, the test should retrieve the relevant DynamoDB record and validate the resulting state.
Typical validations include:
- Record exists
- Partition key is correct
- Sort key is correct
- Required attributes are stored
- Status is updated correctly
- Configuration values match expectations
- TTL is generated correctly
- Expected processing results are persisted
For example:
Insert test data
↓
Application processes data
↓
Retrieve DynamoDB record
↓
Validate final state
This allows the test to validate not only that data can be written, but also that the application's workflow produces the expected result.
Why This Structure Works
The approach separates the test suite into three main responsibilities:
Fixtures
↓
Test Data
Database Helpers
↓
AWS / DynamoDB Operations
Cypress Tests
↓
Business Scenarios + Assertions
This separation provides a cleaner architecture and makes individual components easier to maintain.
If the DynamoDB implementation changes, the test cases do not necessarily need to change.
If the test scenario changes, the AWS helper does not need to change.
Moving Toward Event-Driven Testing
Directly inserting records into DynamoDB is useful for preparing controlled test states, but it can also bypass parts of the application's normal workflow.
This approach can therefore become the foundation for a more complete event-driven testing strategy.
The current approach might look like:
Fixture
↓
Cypress Task
↓
DynamoDB
↓
Application / Verification
The next evolution could be:
Fixture
↓
Event / SNS
↓
Application
↓
DynamoDB
↓
Cypress Verification
Instead of directly creating the database record, the same mock payload can be published as an event.
The application then processes the event through its normal workflow, and Cypress verifies the final database state.
The biggest advantage is that the test-data model can remain reusable.
The injection point changes, but the underlying fixture structure can remain largely the same.
When This Approach Is Useful
This pattern works particularly well for:
- Integration testing
- Event-driven applications
- DynamoDB-backed services
- CI/CD pipelines
- Repeatable test scenarios
- Large-scale automated test suites
- Tests requiring controlled database state
However, direct database seeding should be used carefully for end-to-end testing because it can bypass application logic that you may actually want to validate.
A good testing strategy can use both approaches:
Database seeding for controlled setup and targeted integration tests.
Event/API-driven setup for validating the application's complete workflow.
Key Benefits
Using reusable fixtures together with Cypress tasks provides several advantages:
- Cleaner test code — AWS logic stays outside the test cases.
- Reusable mock data — Fixtures can support many scenarios.
- Reduced duplication — Base data can be overridden when needed.
- Faster execution — Tests do not depend on manual database preparation.
- Better test isolation — Each test can create its own required state.
- Centralized AWS logic — DynamoDB operations are maintained in one place.
- CI/CD friendly — Test data can be generated automatically.
- Easy evolution — The same data model can support future event-driven testing.
Final Thoughts
Automating database setup with reusable fixtures, Cypress tasks, and the AWS SDK can remove a significant amount of repetitive manual work from integration testing.
The key is to keep test data, database operations, and test scenarios separate.
Instead of manually preparing DynamoDB records before every test, the test suite becomes responsible for creating the state it needs and validating the resulting behavior.
More importantly, this approach creates a foundation for moving toward event-driven testing. As the application architecture evolves, the same reusable test data can be injected through events and processed through the application's normal workflow.
The result is a testing architecture that is repeatable, maintainable, scalable, and easier to integrate into CI/CD pipelines.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.