Playwright can drive your browser. It can't test your backend.
Playwright is very good at what it does. If you need to drive a real browser, assert on what a user sees, and catch visual regressions, it is probably the best tool available right now.
This post is not an argument against it. It is about the specific class of bugs Playwright cannot see, why those bugs are the expensive ones, and what a test has to be able to do to catch them.
The test that passes while the system is broken
Here is a scenario most teams have lived through.
A user clicks "Publish". The button works. A success toast appears. Playwright asserts on the toast, the test goes green, CI passes, you ship.
Except the API gateway forwarded the request to the post service with a stale auth header. The post service accepted it anyway, wrote the row with a null author_id, and returned 201. The UI has no idea. It got a 201, so it showed a toast.
Your test asserted on the toast.
Nothing in that flow is Playwright's fault. It did exactly what it was asked to do: drive a browser and check what the user sees. The problem is that the bug was never visible in the browser. It was in the traffic between two services and in a row in Postgres.
Where Playwright's boundary actually sits
Playwright's model is the browser. Everything it does well flows from that.
It can navigate, click, type, wait for selectors, and screenshot. It can make API requests directly through its request context, which is genuinely useful for setup and teardown. It can intercept and mock network calls the browser makes.
What it does not do:
It does not run your services. Playwright assumes something is already serving your app. Getting your services, databases, and dependencies into a known state before the test runs is your problem to solve, usually with Docker Compose and a pile of shell scripts.
It cannot see traffic between services. Playwright's route interception works on requests the browser initiates. When your API gateway calls your post service, that call never touches the browser. It is invisible to the test.
It has no view of your database. You can assert that a page shows "My new post". You cannot assert that the row was written correctly, that the foreign key is populated, or that the write went to the right table.
Mocking third parties means running your own mock. To test a flow that calls Stripe or Auth0, you need to stand up a mock server, point your service at it, and manage its lifecycle. Playwright does not help here because the call happens server-side.
None of this is a criticism. Playwright is a browser automation library and it is excellent at being one. The issue is that most teams are using it as their entire integration testing strategy, and the browser is only one of four layers where things break.
The four layers
Any request in a modern application passes through roughly four layers:
Browser and frontend - what the user actually sees and clicks.
API and backend - the calls between your own services, the headers they carry, the payloads they send.
External APIs p Stripe, Auth0, Twilio, whatever you depend on that you do not control.
Database - what actually got written, and whether it is correct.
Most tools cover one or two of these well:
- Playwright and Cypress cover the browser layer.
- Postman and Bruno cover the API layer, but in isolation rather than as part of a running system.
- Testcontainers gives you real databases and dependencies in Docker, but you assemble the test harness around it yourself.
- Jest and Vitest cover units, with everything interesting mocked away.
Each tool is good. The gap is that a single user action crosses all four layers, and no single tool follows it the whole way. So teams stitch tools together, and the seams between them are exactly where the untested behaviour hides.
What catching that bug actually requires
Go back to the publish flow. To catch the stale auth header and the null author_id, a test needs to do four things in the same run:
- Drive the browser through the publish flow.
- Capture the HTTP call from the web app to the API gateway and assert on its method, URL, and response status.
- Mock the Auth0 JWKS endpoint so the test does not depend on a live third party.
- Query Postgres directly and assert the row exists with the right values.
Doing all four with the usual stack means Playwright, a Docker Compose file, a mock server you maintain, and a database client wired into your test setup. It is possible. It is also brittle, and it is why most teams stop at step one.
What this looks like in Dokkimi
Dokkimi was built for exactly this shape of test. You declare your services, databases, and mocks in YAML, and it spins up an isolated Docker environment for every run: your services, a real database, a headless browser, and interceptor sidecars capturing every HTTP call between services.
The publish flow above becomes a single definition:
name: author-publish-flow
items:
- $ref: ../shared/web-app.yaml
- $ref: ../shared/api-gateway.yaml
- $ref: ../shared/post-service.yaml
- $ref: ../shared/postgres-db.yaml
- $ref: ../shared/mock-auth0-jwks.yaml
tests:
- name: Publish a new post
steps:
# Drive the browser through the publish flow
- action:
type: ui
url: http://web-app:3000/posts/new
subSteps:
- action: type
selector: '#title'
value: 'My new post'
- action: click
selector: '[data-testid="publish-btn"]'
- action: waitForSelector
selector: '[data-testid="success-toast"]'
- action: screenshot
name: post-published
assertions:
# Assert on the traffic between services
- match:
path: $.traffic
where:
- path: $$.origin
operator: eq
value: web-app
- path: $$.request.method
operator: eq
value: POST
- path: $$.request.url
operator: contains
value: api-gateway/v1/posts
count: 1
assertions:
- path: $.match.response.status
operator: eq
value: 201
# Query the database directly
- action:
type: dbQuery
database: postgres-db
query: "SELECT title FROM posts WHERE title = 'My new post'"
assertions:
- count: $.response.data
operator: eq
value: 1
One file. Browser assertion, traffic assertion, mocked third party, and a real database query, all in the same run. The screenshot step also diffs against a baseline, so visual regressions are caught in the same pass.
Your services run unmodified. Dokkimi wires up the sidecars, routing, DNS, browser, and cleanup. There are no code changes on your side.
When Playwright is still the right answer
To be clear about where the line sits.
Use Playwright when you are testing a frontend against an API you do not need to inspect, when your app is largely a monolith with a straightforward database, or when the bugs you keep shipping are genuinely UI bugs.
Use Dokkimi when you run multiple services and the interesting failures happen between them, when you need to assert on database state as part of an end-to-end flow, when your tests depend on third-party APIs you would rather mock properly than stub, or when you have a staging environment that has quietly drifted from production.
Plenty of teams run both. Playwright for focused frontend suites, Dokkimi for the flows that cross service boundaries.
Try it
Dokkimi is free to run locally and in CI.
brew install dokkimi/tap/dokkimi
# or
npm install -g dokkimi
dokkimi init # scaffolds a .dokkimi/ folder with examples
dokkimi validate # check your definitions
dokkimi run # spin up the environment and runRequires Node 20+ and Docker. Run dokkimi doctor after installing to verify your setup.
If you use Claude Code, Cursor, or GitHub Copilot, Dokkimi ships an MCP server that auto-registers on install, so your agent can write, run, and debug test definitions as native tool calls.
The docs are at dokkimi.com/docs, and there is a working Next.js demo with Google OAuth, Postgres, and a full CI pipeline at github.com/dokkimi/demo-nextjs.