The CRAP Metric: Measure PHP Technical Debt with Xdebug Coverage
Use Xdebug coverage reports and the CRAP metric to find the riskiest PHP code in a WordPress plugin or theme, then pay down technical debt one class at a time.
The CRAP metric scores every method in a PHP codebase by combining two numbers: how complex the method is and how much of it is covered by automated tests. Xdebug collects the coverage, PHPUnit calculates the score, and the HTML report ranks your riskiest classes and methods. That gives you a measurable, visual way to pay down technical debt: start with the highest-risk class, add tests, simplify it, and watch the number fall.
We use this method on every PHP project, not just WordPress. On a WordPress site, it works on any code you own: a custom theme, custom plugins, must-use plugins and forked third-party plugins. This guide explains the metric, how to generate the report and how to use it to bring code into good shape step by step.
What the CRAP metric measures
CRAP stands for Change Risk Anti-Patterns. Alberto Savoia and Bob Evans introduced it in 2007 to answer one question: how risky is it to change this code?
Two things make a change risky:
- Complexity. Code with many
ifstatements, loops and early returns has many paths through it. Each path is a way for a change to go wrong. This is measured as cyclomatic complexity, which is roughly the number of independent paths through a method. - No tests. If nothing checks the behavior automatically, a broken path is only found when a customer finds it.
Either one alone is manageable. Simple code without tests is easy to check by reading it. Complex code with good tests is protected. The danger is complex code with no tests, and that is exactly what the CRAP score highlights.
The formula
For a method m:
CRAP(m) = complexity(m)² × (1 − coverage(m))³ + complexity(m)
Coverage is a fraction between 0 and 1. Two things follow from the formula:
- With no tests, the score is the complexity squared plus the complexity. It grows very fast.
- With full coverage, the score drops to the complexity itself. Tests cannot take it lower.
The table below shows how the score changes. The usual threshold is 30. Anything above it deserves attention.
| Complexity | 0% covered | 50% covered | 80% covered | 100% covered |
|---|---|---|---|---|
| 1 | 2 | 1.1 | 1 | 1 |
| 5 | 30 | 8.1 | 5.2 | 5 |
| 10 | 110 | 22.5 | 10.8 | 10 |
| 20 | 420 | 70 | 23.2 | 20 |
| 30 | 930 | 142.5 | 37.2 | 30 |
The table tells you how to fix each kind of problem:
- A complexity of 10 with no tests scores 110. Getting it to around 80% coverage brings it down to about 11. Tests alone fix this one.
- A complexity of 30 never gets below 30, even when fully tested. This method has to be split into smaller ones.
That makes the metric useful for planning. It tells you not only where the risk is, but whether the cure is tests, refactoring or both.
Generating a coverage report with Xdebug
1. Install a coverage driver
PHPUnit needs a coverage driver to know which lines ran during the tests. Xdebug is the most common. Turn on its coverage mode:
XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-html coverage
PCOV is a lighter alternative that is faster for line coverage. Xdebug is the one to use if you also want branch and path coverage.
If you use wp-env for your WordPress test environment, it can start with Xdebug in coverage mode:
npx wp-env start --xdebug=coverage
npx wp-env run tests-cli --env-cwd=wp-content/plugins/my-plugin \
vendor/bin/phpunit --coverage-html coverage
Our guide to automated testing with Playwright and PHPUnit covers setting up wp-env and the WordPress test suite.
2. Tell PHPUnit which code to measure
In phpunit.xml, list the folders you own in the <source> section, and configure the reports:
<source>
<include>
<directory suffix=".php">includes</directory>
<directory suffix=".php">src</directory>
</include>
</source>
<coverage>
<report>
<html outputDirectory="coverage" lowUpperBound="50" highLowerBound="90"/>
<clover outputFile="coverage/clover.xml"/>
</report>
</coverage>
Include every file you want to measure, not only files that already have tests. Untested files then show up as 0% covered instead of being left out, which keeps the report honest.
3. Run it once, even with no tests
You do not need tests to get a useful first report. With a single empty test, every method shows 0% coverage, so its CRAP score is simply complexity squared plus complexity. That already ranks the code by risk and gives you a baseline to improve on.
Reading the report
Open coverage/index.html in a browser. PHPUnit’s HTML report has a few views worth knowing:
- The file tree shows line coverage for every folder and file, colored red, yellow or green using the bounds you set in
phpunit.xml. - The dashboard has charts of coverage and complexity, and two lists: Insufficient Coverage and Project Risks. Project Risks ranks classes and methods by CRAP score. This is your to-do list.
- Each class page shows a table with coverage, complexity and the CRAP score for every method, and the source code with tested lines in green and untested lines in red.
It is a picture of the code’s health. Teams that cannot read PHP can still see the red shrink and the green grow.
Turning the score into a technical debt number
The HTML report is for people. The Clover XML file is for tracking. It records the complexity and CRAP score of every method, so you can count how many methods are over the threshold:
xmllint --xpath "count(//line[@type='method'][@crap > 30])" coverage/clover.xml
Track a few numbers over time:
- Methods above the CRAP threshold. The main debt number. The goal is zero.
- The highest CRAP score. Shows how bad the worst method is.
- Line coverage for the code you own.
These fit into the “untested custom code” metric in our guide to measuring WordPress technical debt. Instead of a rough yes or no for “does this code have tests”, you get a precise number that moves every time someone improves the code.
Paying down the debt, riskiest class first
The report tells you where to start. Work through it in this order.
1. Pick the highest-risk class
Open Project Risks and take the class at the top. Ignore the rest for now. One class fixed properly is worth more than ten classes touched lightly.
2. Write characterization tests
Before changing anything, write tests that record what the code does today, including its odd behavior. These are called characterization tests. Their job is not to prove the code is right. Their job is to tell you the moment a change makes it behave differently.
Use the coverage view of the class as a guide. Every red line is a path with no test yet. Add tests until the important paths are green.
3. Refactor in small steps
With the tests in place, simplify the code. Split long methods into smaller ones, replace nested if statements with early returns and move repeated logic into its own method. Run the tests after every step.
4. Measure again
Run the report again. The class should drop down the Project Risks list, often from the hundreds to single digits. Record the new numbers, commit and move to the next class.
5. Stop new debt from coming in
Run the coverage report in your continuous integration (CI) pipeline on every pull request. Fail the build if the number of methods above the threshold goes up. That way the score only moves in one direction. Our guide to WordPress CI/CD with GitHub Actions shows how to set up the pipeline.
Why this works well with AI coding agents
AI coding agents are good at writing characterization tests and doing mechanical refactors. They are also good at changing behavior by accident. The CRAP workflow gives them guard rails:
- The report tells the agent exactly which class to work on next.
- The characterization tests catch any change in behavior.
- The CRAP score and coverage show whether the change actually reduced risk.
The tests and the numbers stay the same whether the work is done by a person or an agent. That makes the results easy to check.
Applying it to a whole WordPress site
A WordPress site is mostly code you did not write, so start with the code you own:
- Custom themes and child themes. Move logic out of template files and into classes or functions that can be tested.
- Custom plugins and must-use plugins. These are usually the riskiest code on the site, and the best place to start.
- Forked third-party plugins. If you maintain your own fork of an abandoned or overpriced plugin, this is how you make it safe to change. Our guide to what you own when you buy a GPL plugin explains when forking is an option.
Third-party plugins you do not change are better measured with the other technical debt metrics: how outdated they are, whether they are maintained and whether they have known vulnerabilities.
For code that is hard to reach with unit tests, such as templates and hooks, you can also collect coverage while Playwright tests click through the site. This needs a small PHP file that starts coverage collection on every request, but it shows which code your real user journeys actually exercise.
Get your code measured
CRAP analysis is part of our technical debt audit. We generate the coverage report for the code you own, rank the riskiest classes and give you a plan to bring the score down. We use the same approach to make GPL plugin forks safe to maintain, and to keep custom code healthy under our WordPress maintenance plans. Contact us with your site address and what custom code it runs.
Frequently asked questions
- What is the CRAP metric?
- CRAP stands for Change Risk Anti-Patterns. It combines how complex a method is with how much of it is covered by automated tests. A high CRAP score means the code is both complicated and untested, so changing it is likely to break something.
- What is a good CRAP score?
- The common rule is that a method should score 30 or less. Simple methods with good test coverage score close to their complexity, often below 10. Scores in the hundreds are common in older plugins and themes.
- Do I need Xdebug to get a CRAP score?
- You need a code coverage driver. Xdebug and PCOV both work with PHPUnit. PCOV is faster for line coverage, while Xdebug also supports branch and path coverage. PHPUnit calculates the CRAP score from the coverage data.
- Can I use the CRAP metric on a WordPress site?
- Yes. Point the coverage report at your custom theme, custom plugins and must-use plugins, and run your PHPUnit tests. Third-party plugins you have forked can be measured the same way.
- Can more tests always fix a high CRAP score?
- No. Full test coverage brings a method's score down to its complexity, but no lower. A method with a complexity above 30 stays above the threshold even when every line is tested, so it has to be split into smaller methods.