From a wiki page to lived practice in code review
A testing strategy that only lives in a wiki does not change a team's behavior by itself. Only when pyramid thinking is anchored in code review, in the definition of done, and in a gradual migration of existing tests does a healthy balance between fast unit tests and expensive end to end tests actually hold up under deadline pressure.
Table of Contents
- 1. Why a documented testing strategy alone changes nothing
- 2. The test pyramid: basics and common misconceptions
- 3. Anchoring pyramid thinking in code review
- 4. Definition of done as a lever for test distribution
- 5. The E2E trap: easier to reason about, costlier to maintain
- 6. Measuring test distribution: metrics instead of gut feeling
- 7. Incremental migration from unhealthy to healthy
- 8. Anchoring ownership and responsibility in the team
- 9. Test pyramid patterns compared directly
- 10. Summary
- 11. FAQ
1. Why a documented testing strategy alone changes nothing
In almost every team that has ever engaged with the test pyramid, there is a Confluence article or a README file somewhere explaining the principle: many unit tests, fewer integration tests, few end to end tests. Yet the actual suite in practice often looks different, with hundreds of slow Cypress or Playwright tests and barely any unit tests for the actual business logic. The reason is rarely a lack of knowledge. Developers usually know the concept, but knowledge alone does not change behavior when there is no consequence to whether a pull request respects or ignores the pyramid.
A testing strategy that is only documented but never enforced or even made visible loses against the pressure of an approaching sprint deadline in daily work. Anyone who needs to secure a feature quickly under time pressure reaches for the tool that immediately produces understandable confidence, usually a browser test that simulates the full user flow. That feels safer than an isolated unit test with mocks, even though it is more expensive and fragile in the long run. Without a mechanism that influences this decision at the moment of writing, the pyramid remains a diagram in the wiki instead of a lived practice in the code.
2. The test pyramid: basics and common misconceptions
The basic principle of Mike Cohn's test pyramid is simple: unit tests form the wide base because they run fast, are isolated, and point exactly to the broken line of code on failure. Integration tests check the interplay of several components, for example a repository and a database, and form the middle layer. End to end tests simulate real user interactions across the entire application and form the narrow tip, because they are slow, expensive to maintain, and prone to flakiness. The pyramid shape therefore does not describe importance, but the recommended distribution based on execution speed and maintenance cost.
A common misconception is to treat the pyramid as a strict test-count target, for example seventy percent unit tests, twenty percent integration, ten percent end to end. Such numbers are rough orientation, not a mandate to be blindly fulfilled without context. What matters more is the underlying principle: every test should be written at the lowest possible level that reliably catches the relevant failure. The opposite of the pyramid, the so called ice cream cone anti pattern with many manual and E2E tests and barely any unit tests, usually does not emerge deliberately but creeps in through many individually reasonable seeming decisions.
<!-- phpunit.xml: separate test suites per pyramid layer enforce visible separation -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="integration">
<directory>tests/Integration</directory>
</testsuite>
<testsuite name="e2e">
<directory>tests/E2E</directory>
</testsuite>
</testsuites>
<!-- CI runs unit and integration on every commit, e2e only before merge -->
</phpunit>
3. Anchoring pyramid thinking in code review
The most effective lever to bring the test pyramid from paper into everyday practice is code review. Instead of generally asking whether a test exists, a reviewer should specifically ask at which level a test was written and whether that level is appropriate. If a pull request adds a new Playwright test for a pure price calculation that requires no browser interaction at all, that is a legitimate reason for a review comment suggesting a unit test alternative. This moment in review is where culture actually forms, not the documentation in the wiki.
To help reviewers ask this question consistently, a short, concrete checklist as part of the pull request template helps, for example the explicit question of whether the fastest possible test was chosen that reliably covers the failure case. It is important that this rule is not applied dogmatically against every E2E test. A new checkout flow genuinely needs an end to end test, but the edge case check of a discount calculation within that flow belongs in a unit test next to it, not in the same browser test. Reviewers who consistently ask for the lowest sensible level shift the distribution pull request by pull request in the right direction.
4. Definition of done as a lever for test distribution
A definition of done that only requires "tests present" is too unspecific to protect the pyramid. More effective is a definition of done that explicitly distinguishes between levels: business logic needs unit test coverage, interaction with external systems needs an integration test, and only critical user flows justify an additional end to end test. A ticket is only considered done once tests exist at the right level, not once some test is green. This precision turns a vague statement of intent into a verifiable rule that both the developer and the reviewer can check against the ticket.
In the CI pipeline, this rule can additionally be supported technically by defining a budget for the runtime and number of E2E tests per pull request. If a change clearly exceeds this budget, it is a signal that too much logic is probably being tested through the browser instead of at a lower level. The following example shows how such a budget is made visible as part of the pipeline configuration instead of existing only informally in team conversation.
# .github/workflows/test-pyramid-gate.yml
name: Test Pyramid Gate
on: [pull_request]
jobs:
pyramid-budget:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run unit and integration tests (must run on every commit)
run: npm run test:unit && npm run test:integration
- name: Count new E2E test files in this PR
id: e2e_count
run: |
count=$(git diff --name-only origin/main...HEAD -- 'tests/e2e/**/*.spec.ts' | wc -l)
echo "count=$count" >> "$GITHUB_OUTPUT"
- name: Fail if E2E budget is exceeded without justification
if: steps.e2e_count.outputs.count > 3
run: |
echo "This PR adds more than 3 new E2E tests."
echo "Please confirm in the PR description why lower-level tests are insufficient."
exit 1
5. The E2E trap: easier to reason about, costlier to maintain
A central, often underestimated reason for the ice cream cone distribution is a cognitive trap: an end to end test is easier to write, because it simply reproduces what a human would do on screen, click, input, verify. A good unit test, by contrast, requires that the unit under test can even be isolated, which often demands a restructuring with dependency injection or a clean separation of business logic and infrastructure. Anyone who shies away from that restructuring falls back on the browser test, which works around any architectural weakness because it treats the application as a black box.
The problem only shows up later: the E2E test runs slowly, reacts sensitively to timing issues and network latency, and a single broken selector change takes down dozens of tests at once. A unit test for the same edge case would run in milliseconds and would show exactly which calculation is wrong. The short term convenience of the E2E test becomes a long term maintenance burden for the entire suite. The following example shows the same test case once as an expensive browser test and once as a focused unit test for the same business rule.
// BAD: business rule tested only through the full browser flow
test('applies 10% discount for orders over 100 EUR', async ({ page }) => {
await page.goto('/cart');
await page.getByTestId('add-item').click();
await page.getByTestId('qty-input').fill('5');
await page.getByTestId('checkout-btn').click();
await expect(page.getByTestId('discount-line')).toContainText('-10.00');
// Slow, flaky under load, and unclear which rule actually broke
});
// GOOD: same business rule isolated and tested in milliseconds
import { calculateDiscount } from '../src/pricing/discount';
test('applies 10% discount for orders over 100 EUR', () => {
const result = calculateDiscount({ subtotal: 150 });
expect(result.discountAmount).toBe(15);
expect(result.reason).toBe('volume_discount_10');
});
// The E2E flow itself still deserves one happy-path test, just not every rule
6. Measuring test distribution: metrics instead of gut feeling
Without numbers, the discussion about the test pyramid stays subjective, and every team member has their own gut feeling about whether the suite is healthy. A simple but effective script regularly counts how many test files exist per level and how long each level takes in the CI pipeline. These numbers, displayed as a trend over time, objectively show whether the distribution is improving or worsening, regardless of how individual decisions felt during review.
Besides the raw count, the flakiness rate per level is an equally important metric: a team that discovers that twenty percent of its E2E tests occasionally fail without reason, while unit tests are practically never flaky, has a concrete, data backed reason to move logic from the tip of the pyramid to the base. These metrics belong on a dashboard that is visibly discussed in the sprint review or retro, instead of gathering dust in a one time audit report that nobody opens again.
#!/usr/bin/env bash
# scripts/test-distribution.sh - report the current pyramid shape
set -euo pipefail
unit_count=$(find tests/Unit -name '*Test.php' | wc -l)
integration_count=$(find tests/Integration -name '*Test.php' | wc -l)
e2e_count=$(find tests/e2e -name '*.spec.ts' | wc -l)
total=$((unit_count + integration_count + e2e_count))
printf 'Unit: %4d (%d%%)\n' "$unit_count" $((unit_count * 100 / total))
printf 'Integration: %4d (%d%%)\n' "$integration_count" $((integration_count * 100 / total))
printf 'E2E: %4d (%d%%)\n' "$e2e_count" $((e2e_count * 100 / total))
# Fail the pipeline if the pyramid inverts into an ice cream cone
if [ "$e2e_count" -gt "$unit_count" ]; then
echo "[WARN] More E2E tests than unit tests. Pyramid is inverted."
fi
7. Incremental migration from unhealthy to healthy
A team that inherits an existing suite with hundreds of E2E tests and barely any unit tests should never attempt to rewrite everything at once. Such an undertaking ties up weeks of capacity, delivers no functional value in the meantime, and in practice is almost always abandoned after the first urgent feature request. The more effective path follows the strangler principle: every new feature gets pyramid compliant coverage from the start, and existing code is only migrated when it is touched anyway, for example as part of a bugfix or an extension.
To keep this migration from drifting arbitrarily, a rough target ratio per quarter helps, adjusted gradually rather than set as an unrealistic immediate goal. A team currently at five percent unit test share should not set a direct target of seventy percent, but rather define realistic intermediate steps and track them visibly during sprint planning. It is particularly effective to add a focused unit test for the concrete root cause alongside every bugfix in an E2E heavy area, so migration and bugfixing share the same effort instead of being separate work packages.
{
"test_pyramid_roadmap": {
"current_quarter": "2026-Q3",
"baseline": { "unit_pct": 12, "integration_pct": 18, "e2e_pct": 70 },
"targets": [
{ "quarter": "2026-Q3", "unit_pct": 25, "integration_pct": 25, "e2e_pct": 50 },
{ "quarter": "2026-Q4", "unit_pct": 45, "integration_pct": 30, "e2e_pct": 25 },
{ "quarter": "2027-Q1", "unit_pct": 60, "integration_pct": 25, "e2e_pct": 15 }
],
"migration_rule": "new_features_pyramid_compliant_by_default",
"opportunistic_migration": "add_unit_test_when_touching_e2e_covered_code"
}
}
8. Anchoring ownership and responsibility in the team
The test pyramid remains fragile as long as test responsibility is understood as the job of a separate QA role that adds tests at the end of development. A more sustainable model has every developer responsible for the tests of their own code, including the decision about which level a test belongs on. Pairing between experienced and newer team members while writing unit tests transfers practical skill faster than any documentation, because the decision for a test level is worked through live against concrete code.
A rotating role as test steward, not permanently tied to one person, additionally helps to make test debt visible in the retro regularly, without perceiving test quality as one individual's side task. It is important that this role does not absorb responsibility that should actually rest with the whole team, but only prepares the metrics from section six and puts them up for discussion. That way the test pyramid remains a shared concern of the team, not the duty of a single person that immediately dilutes again in their absence.
9. Test pyramid patterns compared directly
The following overview summarizes which concrete decisions favor an unhealthy test distribution and which alternative has proven more sustainable in practice for actually anchoring the pyramid in daily work.
| Situation | Unhealthy pattern | Healthy pattern | Benefit |
|---|---|---|---|
| New business logic | Covered only through one E2E test | Unit test plus one E2E test for the happy path | Fast, precise feedback on regressions |
| Code review | "Test exists" is enough of a criterion | Explicit question about the right test level | Distribution shifts PR by PR |
| Definition of done | Vague wording without level reference | Level specific test requirement per ticket type | Verifiable, non negotiable rule |
| Reworking an existing suite | Big-bang rewrite sprint | Strangler migration on every touch | No standstill, steady progress |
| Test ownership | Separate QA role at the end of the pipeline | Developer owns tests of their own code | Test level decided directly while writing code |
Mironsoft
Test pyramid audits, CI gates and test automation for Magento and Hyva teams
A test pyramid that actually works in daily practice?
We analyze your existing test suite, build code review checklists and CI gates that enforce pyramid thinking, and guide the gradual migration from E2E heavy suites to a healthy test distribution.
Test distribution audit
Build metrics on test levels, runtime and flakiness
CI gate setup
Anchor E2E budget, pyramid checks and definition of done
Migration support
Plan and execute a strangler migration of existing E2E heavy suites
10. Summary
Establishing the test pyramid in the team means translating it out of the wiki into concrete, daily decisions. Code review that specifically asks about the right test level, and a definition of done that distinguishes between unit, integration and E2E requirements, change a team's behavior more reliably than any well written documentation ever could. The E2E trap emerges because browser tests are easier to write in the short term than isolated unit tests, but they cause high long term maintenance costs through flakiness and long runtimes.
An existing unhealthy distribution cannot be fixed overnight, but through a consistent strangler migration, where new features are built pyramid compliant and existing code is migrated when touched, the distribution shifts noticeably over quarters. Objective metrics instead of gut feeling, visibly discussed in the sprint review, and shared responsibility across the whole team instead of an isolated QA role turn the test pyramid into a lived practice instead of a diagram in the wiki.
Establishing the Test Pyramid in the Team, The Essentials at a Glance
Code review as a lever
Explicitly ask about the lowest sensible test level, instead of only checking whether some test exists.
Definition of done
Level specific test requirements instead of vague wording make the rule verifiable.
Avoiding the E2E trap
Browser tests are more convenient short term but more expensive long term than clean unit tests.
Incremental migration
Strangler principle instead of big-bang rewrite, backed by metrics instead of gut feeling.