When upgrading an older suite to PHPUnit 13, how do you replace mock expectations written with withConsecutive() or at()?
answer
- both removed; absent from 13's source
- withParameterSetsInOrder plus exactly(n)
- willReturn(a, b) for return sequences
- willReturnCallback when args decide the answer
- one with() per method per mock
basics
~10 sBoth are gone. Use expects($this->exactly(n)) with withParameterSetsInOrder() to check ordered arguments, willReturn($a, $b) for per-call returns, or willReturnCallback() when the answer depends on the arguments.
solid answer
~30 s`withConsecutive()` and the index matcher `at()` were removed in earlier major versions and do not exist in PHPUnit 13. For ordered argument checks, PHPUnit 13 has `withParameterSetsInOrder()`: `expects($this->exactly(2))->method('send')->withParameterSetsInOrder(['[email protected]', 'Welcome!'], ['[email protected]', 'New sign-up'])`. `withParameterSetsInAnyOrder()` drops the ordering. If you also need a different return per call, chain `willReturn($first, $second)`. When the answer depends on the arguments, `willReturnCallback()` with a `match` is clearer. Do not replace the old code with two `expects()` blocks that each call `with()` on the same method: PHPUnit 13 throws `MethodParametersAlreadyConfiguredForAnotherMatcherException`, because `with()` is an expectation, not a selector.
code
php · 15 lines<?php
declare(strict_types=1);
// Inside a TestCase method
$users = self::createStub(UserRepository::class);
$mailer = $this->createMock(Mailer::class);
$mailer->expects($this->exactly(2))
->method('send')
->withParameterSetsInOrder(
['[email protected]', 'Welcome!', $this->anything()],
['[email protected]', 'New sign-up', $this->stringContains('[email protected]')],
);
$service = new SignUpService($users, $mailer);
$service->register('[email protected]', 'correct horse');go deeper
Know that withConsecutive() and at() no longer exist and that a test using them fails before asserting anything.
Explain withParameterSetsInOrder() with exactly(n), and why willReturnMap() suits lookup-style calls better than expectations.
Plan a suite-wide upgrade: classify each old call, pick the right replacement, and avoid the stacked with() trap.
Decide when interaction-heavy tests should become state checks or fakes rather than be translated call for call.
## What was removed and why it hurts Two old APIs dominate legacy PHPUnit suites: - **`withConsecutive()`** listed one argument set per call: "the first call gets these arguments, the second gets those". - **`at($index)`** was a count rule that matched the *n*-th call to *any* method on the mock, which made tests depend on the global order of every call. Neither exists in PHPUnit 13: a search of its source finds no `withConsecutive()` on the invocation mocker and no `at()` among the count rules on `TestCase`. An old test that uses them dies with an undefined-method `Error` before it can assert anything, so an upgrade usually starts with a wave of these errors. ## The built-in replacement: parameter sets PHPUnit 13's `InvocationMocker` has three methods that take one entry per expected call: | Method | Order of calls | Typical use | |---|---|---| | `withParameterSetsInOrder(...)` | must match the listed order | welcome mail, then ops notification | | `withParameterSetsInAnyOrder(...)` | any order | parallel notifications | | `withParameterSetsInPartialOrder(...)` | sets wrapped as `['pinned' => [...]]` must arrive at their listed position, the rest in any order | a mix of both | Each entry is an array of arguments for one call; a non-array entry is treated as a single-argument call. Each position can be a plain value or a constraint. Two failure modes are worth knowing: 1. More calls than sets: if the count rule still allows the call, it throws `NoMoreParameterSetsConfiguredException` ("Not enough parameter sets configured ..."); with a matching `exactly(n)`, the count rule is applied first and rejects the extra call itself. 2. Fewer calls than sets: verification after the test fails, on the count rule if it expected more calls, otherwise with "Too many parameter sets given ...". Pair the sets with a count rule that matches, usually `exactly(n)`, so the intent is stated twice and both halves agree. ## Return values per call `withConsecutive()` was often chained with `willReturnOnConsecutiveCalls()`. That half survives: `willReturnOnConsecutiveCalls($a, $b)` still exists, and in PHPUnit 13 `willReturn($a, $b)` does the same thing. Remember that the sequence runs out: a third call throws `NoMoreReturnValuesConfiguredException`. ## When the arguments decide the answer Many `withConsecutive()` blocks were really lookup tables: "if called with A return X, with B return Y". Those are better expressed without expectations: - on a stub, use `willReturnMap()` with rows of arguments and a return value; - when logic is needed, use `willReturnCallback()` with a `match` on the arguments; - if you also need to prove each call happened, add `expects($this->exactly(n))` on a mock and keep the callback. ## The tempting wrong fix A common rewrite stacks two expectations: ```php $mailer->expects($this->once())->method('send')->with('[email protected]', 'Welcome!'); $mailer->expects($this->once())->method('send')->with('[email protected]', 'New sign-up'); ``` PHPUnit 13 rejects the second `with()` with `MethodParametersAlreadyConfiguredForAnotherMatcherException`. Its message spells out the reason: `with()` configures an expectation that the method must be called with those arguments; it does not select a return value based on arguments, and `willReturnMap()` is the tool for that. Only one matcher per method may carry an argument rule. ## Ordering across different methods `at()` could assert "`save()` before `send()`". PHPUnit 13 still has `id()` and `after()` on expectations for cross-method ordering, but they have been soft-deprecated since 13.1.0 with no replacement. For new tests, prefer: - asserting on the state the service leaves behind, such as the saved user and a returned result; - a small hand-written **fake** mailer that records messages in an array you can inspect with ordinary assertions. ## Why at() was fragile in the first place `at($index)` counted every call to *any* method of the mock, including queries such as `isAvailable()`. Adding one harmless query to the service shifted every index after it and broke tests that had nothing to do with the change. That brittleness is the practical reason not to recreate index-based matching by hand with a counter inside `willReturnCallback()`. If order genuinely matters, name it explicitly with ordered parameter sets on one method, or check the recorded messages of a fake. ## An upgrade checklist 1. Search the suite for `withConsecutive(`, `->at(`, `setMethods(` and `->any()`. 2. Classify each hit: argument check, return sequence, or lookup table. 3. Rewrite with parameter sets, `willReturn($a, $b)`, or `willReturnMap()`/`willReturnCallback()` respectively. 4. Re-run with `--display-phpunit-deprecations` so PHPUnit's own deprecations show their details. 5. Review each rewritten test for intent: a translation that keeps every old expectation often keeps the old brittleness too.
- What happens if the service sends three mails but withParameterSetsInOrder() lists only two sets?With `exactly(2)` the count rule is applied first, so the third call fails with "... was not expected to be called more than 2 times". If the count rule allowed it, say `atLeastOnce()`, the parameter sets would reject it with `NoMoreParameterSetsConfiguredException`, reporting that not enough parameter sets were configured.
- When is a hand-written fake better than parameter sets?When the order and content of many calls matter, a fake `Mailer` that appends each message to an array lets you assert on the whole list with `assertSame()`. It also survives refactors that change how many times the service calls `send()` internally.
saying these in an interview costs you the question
- withConsecutive() still works in PHPUnit 13 with a deprecation
- Two expects() blocks with different with() replace withConsecutive() cleanly
- with() picks which return value a call gets
- at() is the supported way to check call order in PHPUnit 13
- willReturn() can only take a single value