AI-Generated Code: Build an End-to-End Testing Workflow
An end-to-end testing workflow for AI-generated code checks that a change works in the application users actually run, not just that it looks plausible or passes a unit test. Start with a human-reviewed change, a repeatable test environment, and a defined user journey; the workflow below connects those pieces to pull requests and deployment.
What You’ll Need
- A version-controlled repository and a way to run its application and tests locally or in continuous integration (CI).
- An existing test runner or framework, plus a browser automation tool if the product has a web interface.
- A documented setup command, test data strategy, and separate test environment that does not contain real customer data.
- Access to CI checks and a team rule for reviewing AI-assisted changes before they are merged.
- A small, representative set of critical user journeys and expected outcomes.
Step-by-Step
- Define what “end to end” means for the change. Describe the user-visible behavior in plain language, including the starting state, actions, and expected result. For example: “A signed-in user submits a valid order and sees a confirmation with the correct order number.” Include important failure cases, such as invalid input or an unavailable dependency. Avoid vague goals like “test the checkout.”
- Review the generated code before testing it. Treat AI output as an untrusted draft, not as a specification. Inspect the diff for unintended files, unsafe data handling, hard-coded credentials, dependency changes, and behavior that does not match the requirement. Confirm what the code is supposed to do with the author or task description. Tests can pass while verifying the wrong behavior.
- Make setup and execution repeatable. Record the supported runtime and dependencies in the project’s existing configuration, and provide clear commands to start the application and run checks. Pin or lock dependencies where the project supports it. Ensure the test environment can be created from a clean checkout; do not rely on a developer’s local database, browser state, or uncommitted files.
- Prepare isolated test data and dependencies. Use disposable databases, seeded fixtures, or API stubs appropriate to the application. Give each run predictable data and clean it up afterward. Keep tests away from production services and real user records. If a test needs an external service, make the boundary explicit and use a controlled test endpoint or a reliable stub rather than silently making live calls.
- Test at the right layers before the browser journey. Run formatting, static checks, and focused unit or integration tests first. These checks identify small defects quickly and make end-to-end failures easier to diagnose. Add coverage for important business rules and API boundaries, but do not duplicate every internal implementation detail in browser tests.
- Automate a small number of critical user journeys. Use the project’s established browser or device automation framework. Write tests around observable behavior: navigate to a page, enter data, submit, and assert a meaningful result. Prefer accessible labels or stable test identifiers over brittle selectors tied to layout or styling. Wait for a real condition, such as a confirmation becoming visible, rather than sleeping for a fixed number of seconds.
- Check both success and failure behavior. Cover at least the main successful path and the most consequential validation or error path for the change. Assert useful messages and ensure an unsuccessful action does not create partial or duplicate data. Keep tests focused: a failing assertion should point clearly to the behavior that regressed.
- Run the workflow locally from a clean state. Follow the documented setup, start required services, reset test data, and run the same commands CI will use. Repeat a flaky test enough to investigate whether timing, shared state, network access, or random data is involved. Do not “fix” a failure by weakening assertions until the test passes.
- Make CI run checks on every proposed change. Configure the repository’s CI to install dependencies, prepare isolated services and data, run fast checks first, then run the end-to-end suite. Preserve logs, screenshots, or traces when a browser test fails. Set required checks so a failed or incomplete run blocks merging; report skipped tests clearly rather than treating them as successful.
- Review test quality and merge deliberately. Ask whether each new test would fail if the intended behavior broke. Review assertions, cleanup, secrets handling, and whether the test depends on execution order. Merge only after required checks pass and a person has reviewed the generated code and test changes. After deployment, monitor the relevant application behavior and retain a rollback or recovery process appropriate to the system.
Common Mistakes
- Accepting generated tests without checking them: A test can repeat the implementation’s mistake or assert only that a page loaded. Compare each assertion with the requirement and expected user outcome.
- Relying only on end-to-end tests: Browser tests are slower and can be harder to diagnose. Use unit and integration checks for detailed rules, reserving end-to-end tests for valuable journeys across system boundaries.
- Using production data or services: This risks data exposure and accidental real-world actions. Isolate credentials, endpoints, databases, and test accounts.
- Writing brittle tests: Selectors based on element position, arbitrary delays, or exact styling can fail after harmless UI changes. Assert stable, user-relevant outcomes instead.
- Making the suite too large for its purpose: Many overlapping journeys increase maintenance and slow feedback. Prioritize critical paths and add cases when they cover distinct risk.
- Ignoring flaky failures: Retrying indefinitely can hide real defects. Track recurring failures, diagnose the cause, and quarantine a test only with a named owner and a plan to restore it.
Tips
- Keep the task description, acceptance criteria, and tests aligned; update all three if the intended behavior changes.
- Use deterministic clocks, random seeds, and test fixtures when those inputs affect the result.
- Keep secrets in the CI platform’s secret store and avoid printing them in logs or screenshots.
- Run quick checks on every pull request and schedule broader, slower suites separately when they are not practical on every change.
- Record test ownership and make failures actionable with the failing journey, environment details, and diagnostic artifacts.
Troubleshooting
A test passes locally but fails in CI: Compare runtime and dependency versions, environment variables, time zone, browser configuration, and service readiness. Reproduce the CI commands in a clean environment and remove reliance on local state.
A browser test fails intermittently: Inspect the trace, screenshot, and logs. Check for missing condition-based waits, shared test data, animations, race conditions, or an overloaded CI worker. Fix the cause; use retries only as a temporary diagnostic aid.
The app cannot connect to a service: Confirm the service starts before the test, its test URL and credentials are configured, and health checks indicate readiness. Verify that the run uses test resources, not production.
Tests pass but users still find a defect: Revisit the acceptance criteria and add a test for the missing observable behavior. Check whether the test exercised the real integration boundary or merely mocked it, and add an appropriate integration or end-to-end check.
Generated code changes unrelated behavior: Pause the merge, inspect the full diff, and reduce the change to the requested scope. Add regression coverage for affected behavior rather than relying on a broad “all green” result.
Frequently Asked Questions
Should every AI-generated change get an end-to-end test?
Not necessarily. Add one when the change affects an important user journey or crosses system boundaries. Small internal changes may be adequately covered by focused unit or integration tests.
Can AI write the end-to-end tests too?
It can draft them, but a reviewer should verify the requirements, selectors, test data, and assertions. Generated tests are not evidence of correctness until their behavior is understood.
How many end-to-end tests should a project have?
There is no universal number. Cover distinct, high-value journeys and important failure cases, and avoid duplicating checks that are clearer and faster at lower test layers.
Should flaky tests block a merge?
A flaky test should be investigated, not routinely ignored. If it must be temporarily quarantined, document the risk, assign an owner, and keep other reliable checks required.
What should CI save when a test fails?
Save enough to reproduce and diagnose the failure, such as the test name, logs, screenshot, browser trace, and relevant environment information. Ensure artifacts do not expose secrets or personal data.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.