skip to content

With PHPUnit 13, how do --filter and --testsuite narrow a test run, and what pattern syntax does --filter accept?

level: middleimportance: should knowfreq 42%

answer

  1. suite names versus test names
  2. comma-separated suite list
  3. case-insensitive regular expression
  4. matched against Class::method with data set
  5. #2, #2-4 and @name select data sets

basics

~10 s

--testsuite selects named suites from phpunit.xml; --filter selects individual tests by a case-insensitive regular expression matched against Class::method plus any data-set name, with shortcuts such as #2 or @name for data sets.

solid answer

~40 s

`--testsuite unit` (or `unit,integration`) runs only the named `<testsuite>` elements; `--exclude-testsuite` removes some. `--filter` works inside that selection, test by test: its pattern is matched case-insensitively against the full test name, `Fully\Qualified\ClassTest::testMethod`, plus `with data set ...` for provided data. A plain word such as `--filter testRefund` is wrapped into a regex; a pattern with its own delimiters is used as-is. Shortcuts target data sets: `testRefund#2` picks set 2, `#2-4` a range, and `testRefund@"partial refund"` a named set. `--exclude-filter` removes matches. A plain filter still calls every data provider in the loaded files, because data-set names are unknown until providers run. `--list-tests` shows what a selection would run.

code

bash · 8 lines
bash
# one suite, then one method, then one named data set
vendor/bin/phpunit --testsuite unit
vendor/bin/phpunit --filter 'InvoiceTest::testRefund$'
vendor/bin/phpunit --filter 'testRefund@partial refund'

# data sets 2 to 4, and a preview of what would run
vendor/bin/phpunit --filter 'testRefund#2-4'
vendor/bin/phpunit --testsuite integration --list-tests

go deeper

for a junior

Recall that --testsuite picks named suites from phpunit.xml and --filter picks tests by name.

for a middle

Explain what --filter matches - a case-insensitive regex over Class::method with data set - and the #N, #N-M and @name shortcuts.

for a senior

Show how you split a legacy suite for CI stages, and why filtered runs skip dependent tests and can still call data providers.

for a principal

Decide which selections belong in configuration and which stay ad hoc, so CI jobs cannot shrink silently as tests are renamed.

## Two different levels of selection PHPUnit 13 has two independent ways to run less than the whole suite, and they operate on different things. - `--testsuite` selects **named suites** declared in `phpunit.xml`, such as `unit` or `integration`. It is coarse and cheap. - `--filter` selects **individual tests** by name, inside whatever set of files is being loaded. They combine: `vendor/bin/phpunit --testsuite integration --filter Refund` loads only the integration suite and then keeps only tests whose names match `Refund`. ## `--testsuite` and `--exclude-testsuite` The argument is a suite name, or several separated by commas: `--testsuite unit,integration`. `--exclude-testsuite slow` runs everything except that suite. Without either option PHPUnit runs the suite named by the root `defaultTestSuite` attribute if there is one, otherwise all suites. A suite may carry its own `bootstrap` script; it is loaded only when that suite is selected, which is one practical reason to split suites instead of filtering. ## What `--filter` matches Every test has a full name of the form: ``` App\Tests\Unit\InvoiceTest::testRefund with data set "partial refund" ``` The filter is a **regular expression**, matched **case-insensitively**, against that string. | Filter | Selects | |---|---| | `--filter testRefund` | any test whose name contains `testRefund` | | `--filter 'InvoiceTest::testRefund$'` | that method only, no data sets | | `--filter 'testRefund#2'` | data set `#2` of `testRefund` | | `--filter 'testRefund#2-4'` | data sets 2 to 4 | | `--filter 'testRefund@partial refund'` | the data set named `partial refund` | | `--filter '/^App\\Tests\\Unit\\/'` | a delimited regex used as written | A plain pattern starting with a letter or digit is wrapped into a case-insensitive expression; PHPUnit deliberately does not escape it, so regex characters such as `.` and `$` keep their meaning. A pattern that is already a delimited regular expression is used unchanged. `--exclude-filter` takes the same syntax and removes matches. ## Practical consequences 1. **Quote the pattern in the shell.** `#`, `$`, spaces and backslashes all mean something to the shell before PHPUnit sees them. 2. **Namespaces need escaping.** Inside a regex a single backslash escapes the next character, so matching a namespace separator needs `\\` in the pattern. 3. **Filtering a dependent test skips it.** If the selected test depends on another through `#[Depends]`, the producer is not pulled in; the dependent test is skipped because its producer did not pass in this run. 4. **Data providers still run.** PHPUnit calls providers while building the suite, and a plain filter is matched against names that include data-set labels, which only exist once the provider has run. So even `--filter testRefund` calls every provider in the loaded files. Only a filter with a data-set portion (`testRefund#2`, `testRefund@name`) lets PHPUnit skip the providers of methods whose names cannot match. 5. **Check before you trust.** `--list-tests` prints the tests a selection would run, which is quicker than discovering a typo from "No tests executed!". ## Where this fits in CI For a legacy project brought into CI, suites are the stable, reviewed split - `unit` on every push, `integration` in a later stage. `--filter` is a local debugging tool: re-run one failing method or one data set while you fix it. A CI job built on long `--filter` expressions is fragile, because renaming a test silently drops it from the job. ## Related options - `--group` and `--exclude-group` select by `#[Group]` attributes instead of by name. - `--test-id-filter-file` runs exactly the tests listed by ID in a file, one per line, which suits tooling that computes a selection. - `--stop-on-failure` ends the run at the first failure, useful together with a filter while iterating. ## A worked debugging loop A typical local session on a legacy billing test looks like this: the CI log names `InvoiceTest::testRefund with data set "partial refund"` as failing. Locally you run `--filter 'testRefund@partial refund'` to execute just that case, add `--stop-on-failure` while iterating, and once it passes widen the filter to `'InvoiceTest'` and then drop it, so the fix is checked against the neighbouring tests before you push. ## Summary Use `--testsuite` for the coarse split declared in configuration and `--filter` for pinpointing tests and data sets by name. Remember that `--filter` is a case-insensitive regex over `Class::method with data set ...`, so quote it and escape it deliberately.

  • With PHPUnit 13, why does --filter testrefund also run testRefundPartially and testRefund with every data set?
    The filter is an unanchored, case-insensitive regular expression over the full name `Class::method with data set ...`. `testrefund` matches any name containing that text in any case. Anchor it - `'::testRefund$'` - to select the method without data sets, or use `testRefund#N` or `@name` for a specific set.
  • In PHPUnit 13, when is splitting tests into suites better than relying on --filter in CI?
    When the split is permanent and reviewed: unit versus integration, fast versus slow. Suites live in `phpunit.xml`, can have their own bootstrap, and a renamed test stays in its suite. A CI filter expression silently loses tests when names change, and nobody notices the job got smaller.

saying these in an interview costs you the question

  • Thinks --filter matches only the method name, not the class
  • Believes the --filter pattern is case-sensitive
  • Expects --filter to pull in a #[Depends] producer automatically
  • Uses --testsuite with a directory path instead of a suite name
  • Builds CI stages on long --filter expressions instead of suites