For those of you who have stopped thinking and blamed it on the network during the E2E test. Techniques to get out of the mud with Playwright
"The CI is red again. I thought I passed yesterday..."
If you're thinking about it while looking at the log, let me tell you something that might hurt your ears. Assigning the cause of test failure to ``network fluctuations'' or ``execution environment load'' is the worst thing you can do as an engineer to stop thinking.
Playwright is certainly a powerful tool, but it only works if the user properly understands the asynchronicity of the system. If you're being swayed by tools and embedding `waitForTimeout` with enthusiasm, you'll remain a junior engineer for the rest of your life.
Escape from the quagmire of E2E testing and develop a "realistic survival strategy" to accelerate development.
1. Don't rely too much on Auto-waiting. The essence of Web-First Assertion
``Waiting until an element appears'' is the basic principle of Playwright, but don't underestimate the complex rendering of SPA. There is a difference between an element existing in the DOM and being able to manipulate it.
A particularly dangerous pattern is performing UI operations without waiting for an API response. If you write code that "wait for 3 seconds" in this case, you are creating a breeding ground for bugs yourself.
Bad example: test of chance
# 3秒待てばいいだろうという甘え。 This is the cause of Flaky Test
page.click("button#submit")
`
Good example: State management with Web-First Assertion
Playwright's `expect` automatically polls (retries) until the condition is met.これを使わない手はない。from playwright.sync_api import expect
# Wait until the button is enabled and make sure to click it
submit_btn = page.locator("button#submit")
expect(submit_btn).to_be_enabled()
submit_btn.click()
`
Designing to wait for this "determination of the state" is the first step toward making the test deterministic.
2. Parallel execution trap: How to control DB contention
It is common practice to use `shard` to shorten CI execution time. However, if you run it in parallel in an environment where test data is shared, DB contention will definitely occur.
The golden rule of parallelization is ``independence of each test''.
- Database Seeding: Before executing a test, create data specifically for that test case via API.
- Cleanup: `after_each` hook to ensure that created records are deleted.
# Create a unique user for each test and delete it after the test
@pytest.fixture(scope="function")
def test_user(api_request_context):
user = api_request_context.post("/api/test/setup-user")
yield user
api_request_context.post(f"/api/test/cleanup/{user.id}")
`
If you find this too troublesome, you are not qualified to write E2E tests. Testing that does not maintain the integrity of the infrastructure is nothing more than noise that diminishes the credibility of the product.
3. The strongest separation called “API mock”
The only way to physically eliminate the "it crashes because the network is slow" excuse is to mock external APIs.
E2E testing is the place to verify ``UI behavior.'' Testing whether the backend API is down or the response is slow is the role of integration tests and API tests, and is not the responsibility of E2E.
In modern development, it is common sense to use MSW (Mock Service Worker) or Playwright's `page.route`.
# Fix API response and focus on UI validation
page.route("**/api/v1/user", lambda route: route.fulfill(
status=200,
content_type="application/json",
body='{"name": "Test User", "role": "admin"}'
))
`
This completely eliminates network fluctuations. If the test fails, it's 100% your UI code's fault. Let's admit it honestly.
4. Use “evidence” instead of “hunch” when debugging
When a test fails, the most troublesome case is when it fails only in CI without being reproduced locally. Don't do something stupid like loading `console.log` and re-running it here.
Playwright comes standard with the most powerful debugging tools.
1. Trace Viewer: Track all operations during test execution, network logs, and DOM snapshots in chronological order. If you look at this, it will be obvious why the element was not found.
2. UI Mode: Start with `npx playwright test --ui` and repeat execution, modification, and re-execution step by step.
Complaining about ``unstable tests'' without using these tools is like standing on a battlefield without a weapon.
Finally: Tests should be “breakwaters”
Don't have time to write test code? Have you ever calculated which is cheaper, the cost of manually hitting the screen to miss the degradation or dealing with the problem late at night?
Testing is not a hindrance to development. It is a ``breakwater'' for releasing products to the world with peace of mind.
If your tests aren't stable, it's not your code's fault. You can't control the state of the system, it's because your design is lax.
When a "test fails", do you blame it on the environment or fix it as a design flaw? That difference is the dividing line between junior and senior. Now let's remove `waitForTimeout` and rewrite our test code to be more robust.