With PHPUnit 13, how do --filter and --testsuite narrow a test run, and what pattern syntax does --filter accept?
answer
- suite names versus test names
- comma-separated suite list
- case-insensitive regular expression
- matched against Class::method with data set
- #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# 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-testsgo deeper
Recall that --testsuite picks named suites from phpunit.xml and --filter picks tests by name.
Explain what --filter matches - a case-insensitive regex over Class::method with data set - and the #N, #N-M and @name shortcuts.
Show how you split a legacy suite for CI stages, and why filtered runs skip dependent tests and can still call data providers.
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