How @group and --filter help find the right subset among thousands of tests
A test suite with a few hundred tests runs completely in seconds, and nobody thinks about subsets. But once a project grows to several thousand tests, running the entire suite for every small change becomes impractical. PHPUnit's @group attributes and the command line's --filter parameter let you run exactly the tests relevant to the current change, for example only fast unit tests or only a specific module, provided the team agrees on a consistent taxonomy.
Table of Contents
- 1. Why large suites need targeted selection
- 2. Categorizing tests with @group
- 3. Targeted selection with --filter
- 4. Test suites in phpunit.xml as a coarse first layer
- 5. Designing a sensible group taxonomy for a team
- 6. Using groups and filters deliberately in the CI pipeline
- 7. Filters in the local developer workflow
- 8. Capturing common combinations as Composer scripts
- 9. Maintaining the taxonomy instead of letting it grow wild
- 10. Summary
- 11. FAQ
1. Why large suites need targeted selection
In a small project with a few hundred tests, the question of which tests to run is practically irrelevant: everything finishes completely in a few seconds. In a grown project with several thousand tests, including slow integration tests, database tests, and external API calls, a full run can instead take ten minutes or longer. Running the entire suite for every small change is then neither practical nor necessary, since the vast majority of changes only touch a small part of the codebase.
This is exactly where two complementary PHPUnit mechanisms come in: the @group attribute, which lets tests be categorized by content, and the --filter parameter, which selects tests by name or class. Both mechanisms solve different problems: groups suit recurring, thematic subsets like fast versus slow, filters suit one-off, ad hoc selections during active development on a single feature.
2. Categorizing tests with @group
The @group attribute (or the Group attribute in newer PHPUnit versions) is placed above a test class or an individual test method and assigns the test to one or more named groups. A test method can belong to several groups at once, for example being marked as both unit and checkout, which allows flexible combinations later during selection.
On the command line, the --group parameter runs only a specific group, while --exclude-group explicitly excludes a group. Both parameters can be combined and specified multiple times, so for example all tests in the unit group except those in the legacy group can run, which in practice allows very precise, repeatable subsets without ever having to search through the test files themselves.
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
#[Group('checkout')]
final class CheckoutTotalCalculatorTest extends TestCase
{
#[Group('unit')]
#[Group('fast')]
public function testCalculatesTotalWithoutDiscount(): void
{
$calculator = new CheckoutTotalCalculator();
self::assertSame(4999, $calculator->calculate(4999, null));
}
#[Group('integration')]
#[Group('slow')]
public function testCalculatesTotalWithLiveTaxServiceCall(): void
{
$calculator = new CheckoutTotalCalculator(new LiveTaxRateClient());
self::assertGreaterThan(0, $calculator->calculate(4999, 'DE'));
}
}
// CLI: only fast unit tests in the checkout area
// vendor/bin/phpunit --group checkout --group unit
// CLI: everything except the slow integration tests
// vendor/bin/phpunit --exclude-group slow
3. Targeted selection with --filter
While @group is a permanent categorization stored in the code, the --filter parameter works ad hoc on the command line, without the test code needing to know anything about groups. The filter accepts a regular expression checked against the fully qualified name of each test method, consisting of class name and method name. That makes it ideal for the situation where a developer is working on a single class and wants to see only its tests, without setting up a group first.
A common pattern is to combine the filter with the name of the class currently being worked on, or even just a single method, during a TDD cycle, for example, to get feedback on a single test method within milliseconds instead of rerunning the whole class or even the whole suite every time. Once the work at that spot is done, the full suite or at least the relevant group runs again to make sure nothing else broke.
// Run only the CheckoutTotalCalculatorTest class
vendor/bin/phpunit --filter CheckoutTotalCalculatorTest
// Run only a single test method (regular expression)
vendor/bin/phpunit --filter '::testCalculatesTotalWithoutDiscount$'
// All tests whose name contains "Discount", across the whole suite
vendor/bin/phpunit --filter Discount
// Combining filter and directory to narrow the search
vendor/bin/phpunit --filter Discount tests/Unit/Checkout
4. Test suites in phpunit.xml as a coarse first layer
Besides groups and filters, phpunit.xml offers a third organizational layer: named testsuite blocks, each referencing its own directories. This layer is coarser than groups, since it is tied to directory structure rather than content-based categorization, but it fits well for the basic split between unit tests, integration tests, and functional tests, which in many projects already live in separate directories anyway.
In practice both layers complement each other: the testsuite structure separates roughly by directory and technical test type, while @group enables finer, domain-specific categories such as checkout or inventory within that coarse structure. A developer can first narrow down to unit tests via --testsuite and then further filter by a specific domain module via --group.
<!-- phpunit.xml -->
<phpunit bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="integration">
<directory>tests/Integration</directory>
</testsuite>
<testsuite name="functional">
<directory>tests/Functional</directory>
</testsuite>
</testsuites>
</phpunit>
<!-- CLI: run only the unit test suite -->
<!-- vendor/bin/phpunit --testsuite unit -->
5. Designing a sensible group taxonomy for a team
The biggest risk with @group is not the mechanics, it is a wildly growing, inconsistent taxonomy: one developer names a group slow, another names it langsam, a third invents integration-slow for the same purpose. Without team agreement, the group structure quickly becomes useless, because nobody can be sure which groups actually exist and what they mean. A good taxonomy is therefore short, documented, and limited to two or three non-overlapping dimensions.
A proven basic structure distinguishes by speed (such as fast and slow), by test type (such as unit, integration, and functional), and optionally by domain module (such as checkout, inventory, and customer). These three dimensions can be combined freely without the number of groups exploding, because each test method simply gets whichever groups apply to it, usually two to three per method.
6. Using groups and filters deliberately in the CI pipeline
A well thought out taxonomy pays off most in the CI pipeline. On every push to a feature branch, only the fast group can run first, giving initial feedback within a few seconds. Only when merging into the main branch, or in a separate, parallel pipeline stage, do the slow and integration groups run as well, which are allowed to take longer overall since they do not need to run on every single commit.
This staged approach significantly reduces the average wait time for developers without reducing test coverage, since in the end all tests still run, just staggered in time according to their priority for fast feedback. What matters is that the slower second stage runs reliably and visibly for everyone, so it does not get forgotten and end up with nobody regularly running the full suite after all.
# .gitlab-ci.yml (excerpt)
fast-feedback:
stage: test
script:
- vendor/bin/phpunit --group fast
full-suite:
stage: test
only:
- main
- merge_requests
script:
- vendor/bin/phpunit --exclude-group fast
- vendor/bin/phpunit --group fast
7. Filters in the local developer workflow
In day-to-day local work, --filter is usually the more practical tool, since it requires no prior categorization in the code. While a developer works on a bug fix in a specific class, a single command can isolate exactly the relevant test class, drastically shortening the feedback loop of TDD. Many IDEs, including PhpStorm, offer built-in buttons for this that automatically generate the matching --filter call in the background, so the developer never has to type the syntax by hand.
A good workflow combines both tools depending on the situation: --filter for one-off work on a single class during development, --group for the deliberate, documented split of the suite in the CI pipeline and for regularly recurring local runs such as a daily smoke test using only the fast group.
8. Capturing common combinations as Composer scripts
So the right group and filter combination does not get reinvented or mistyped by every developer separately, it is worth storing the most common invocations as Composer scripts in composer.json. A command such as composer test:fast then always internally runs the same, team-agreed PHPUnit invocation, regardless of whether any individual developer remembers the exact group syntax.
That not only reduces typos, it also makes the group taxonomy discoverable for new team members: a glance at composer.json immediately shows which sensible subsets exist, without anyone having to read the full taxonomy documentation. Changes to the group structure then only need to be maintained in one central place, instead of drifting apart across every individual developer's command line habits.
{
"scripts": {
"test:fast": "vendor/bin/phpunit --group fast",
"test:full": "vendor/bin/phpunit",
"test:checkout": "vendor/bin/phpunit --group checkout",
"test:no-legacy": "vendor/bin/phpunit --exclude-group legacy"
}
}
// Invocation: composer test:fast
9. Maintaining the taxonomy instead of letting it grow wild
A group taxonomy is not a one-time project, it requires ongoing maintenance. New modules get new domain groups, old groups that are no longer needed should be actively removed instead of lingering as dead weight in the code. A simple grep across the codebase for all used group names, run regularly, quickly reveals when typos or variants have crept in, such as slow next to a differently spelled equivalent in the same codebase.
The table below summarizes a proven, three-dimensional taxonomy that has worked well in many medium to large PHP projects and can serve as a starting point for a team's own convention.
| Dimension | Example Groups | Purpose | Typical Usage Point |
|---|---|---|---|
| Speed | fast, slow | Separate fast feedback from slower runs | CI pipeline, staged execution |
| Test type | unit, integration, functional | Distinguish technical test level | phpunit.xml testsuites, CI stages |
| Domain module | checkout, inventory, customer | Work deliberately on one area | Local development, feature branches |
| Special cases | legacy, flaky-retry | Make deliberately marked exceptions visible | Exclude group in standard runs |
Mironsoft
Test automation, Magento quality assurance, and CI integration
Tests that catch real bugs instead of just turning green?
We review existing PHPUnit suites for implementation-detail tests, flaky tests, and missing coverage at critical points, then build a test strategy that provides real confidence with every Magento update.
Test Audit
Reviewing existing suites for mocking antipatterns and blind spots.
Test Strategy
Meaningfully combining unit, integration, and MFTF tests for Magento projects.
CI Integration
Setting up fast, reliable test runs in GitLab CI or GitHub Actions.
10. Summary
Test Groups and Filters in PHPUnit: The Essentials at a Glance
@group
Permanently assigns tests to one or more named groups in code, selectable via --group and --exclude-group.
--filter
Ad hoc selection via regular expression on class and method names, ideal for one-off work on a single test class.
testsuite in phpunit.xml
A coarse, directory-based layer for the basic split between unit, integration, and functional tests.
Taxonomy
Short, documented, and limited to two or three non-overlapping dimensions, otherwise the group structure quickly grows wild.