Test Groups and Filter Strategies for Large PHPUnit Suites
AI generated
@test
assert
PHPUnit · Test Selection · Scaling
Test Groups and Filter Strategies for Large Suites
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.

15 min read Test Groups · Filter · CLI PHPUnit 10 · 11 · PHP 8.x

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.

11. FAQ: Test Groups and Filters in PHPUnit: The Essentials at a Glance

1What is the difference between @group and --filter?
@group is a permanent categorization stored in code, selected via --group and --exclude-group. --filter is an ad hoc selection using a regular expression on test names, without the code needing to know anything about groups.
2Can a test method belong to multiple groups at once?
Yes, a method can carry any number of groups, for example unit, fast, and checkout at the same time, which allows flexible combinations during selection on the command line.
3How do I explicitly exclude a group from a test run?
With the --exclude-group parameter followed by the group name. It can be combined with --group, for example to run all unit tests except those marked as flaky.
4Is --filter limited to class or method names?
The filter checks a regular expression against the fully qualified test name, made up of class and method, and can therefore address both levels depending on how specific the expression is written.
5What is the difference between a testsuite in phpunit.xml and a @group?
A testsuite is a coarse, directory-based split, a @group is a finer, content-based categorization independent of directory. Both layers complement each other and can be combined.
6How do I prevent the group taxonomy from growing wild across a team?
With a short, documented convention using a few clearly separated dimensions, plus regular checks via grep across all used group names to catch typos and duplicates early.
7Should I always run the full suite in the CI pipeline?
Not on every single push. A staged approach with a fast group for immediate feedback and a full, later stage for merge requests reduces wait time without reducing coverage.
8Can I use groups and filters together in a single call?
Yes, PHPUnit applies both conditions in combination, so for example a specific group can additionally be narrowed further by filtering on part of a name.
9How many groups should a single test method ideally have?
In practice, two to three groups per method have worked well, one each from the speed, test type, and optionally the domain dimension, more groups usually make things harder to follow rather than clearer.
10Is --filter suitable for permanent use in the CI pipeline?
Not really as the sole mechanism, since filter expressions can become unwieldy and offer no permanent, documented structure. Groups and testsuite definitions are better suited for CI.