-
-
Notifications
You must be signed in to change notification settings - Fork 101
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4bb7399
commit 02aa090
Showing
3 changed files
with
236 additions
and
0 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
231 changes: 231 additions & 0 deletions
231
...ces/posts/2024/2024-10-08-how-to-upgrade-deprecated-phpunit-with-consecutive.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
--- | ||
id: 417 | ||
title: "How to Upgrade deprecated PHPUnit withConsecutive()" | ||
perex: | | ||
The `withConsecutive()` method [was deprecated in PHPUnit 9](https://github.com/sebastianbergmann/phpunit/issues/4255#issuecomment-636422439) and removed in PHPUnit 10. It sparked many [questions](https://stackoverflow.com/questions/75389000/replace-phpunit-method-withconsecutive-abandoned-in-phpunit-10), [on StackOverflow](https://stackoverflow.com/questions/77865216/phpunit-withconsecutive-is-gone-what-is-the-recommended-approach), in [various projets](https://www.drupal.org/project/drupal/issues/3306554) and [GitHub](https://github.com/search?q=repo%3Asebastianbergmann%2Fphpunit+withConsecutive&type=issues). | ||
It was not very popular BC break. There is no 1:1 replacement. It can be combined with `willReturn*()` methods and that can make it even more tricky to merge with. | ||
PHPUnit upgrades take 95 % of time just to upgrade this single method, and 5 % about everything else. | ||
Recent months we've done couple of project upgrades with Rector and we've learned a lot. | ||
Today, I want to share bit of knowledge with you and explain, **why it's a change for better code**. | ||
--- | ||
|
||
What does `withConsecutive()` method actually do? | ||
|
||
```php | ||
$mock = $this->createMock(MyClass::class); | ||
$mock->expects($this->exactly(2)) | ||
->method('someMethod') | ||
->withConsecutive( | ||
['first'], | ||
['second'] | ||
); | ||
``` | ||
|
||
It defines what arguments are on the input, once the method mock is called. E.g. here: | ||
|
||
* on 1st call, it expects `['first']` | ||
* on 2nd call, it expects `['second']` | ||
|
||
To be honest, I've never wrote such code myself, but so far we found it in every code base we've upgraded. | ||
It's been available since 2006, and only removed after 16 years in 2022. | ||
|
||
<br> | ||
|
||
So how can be replace it? It would be very convenient, if there would some kind of `withNthCall()` method: | ||
|
||
```php | ||
$mock = $this->createMock(MyClass::class); | ||
$mock->expects($this->exactly(2)) | ||
->method('someMethod') | ||
->withNthCall(1, ['first']) | ||
->withNthCall(2, ['second']); | ||
``` | ||
|
||
But it's not. | ||
|
||
|
||
## `withCallable()` to the Rescue | ||
|
||
Instead we use `withCallable()` trick. This methods accepts the called parameters, that we can assert inside. | ||
|
||
```php | ||
$mock = $this->createMock(MyClass::class); | ||
$mock->expects($this->exactly(2)) | ||
->method('someMethod') | ||
->withCallable(function ($parameters) { | ||
// check the parameters here | ||
}); | ||
``` | ||
|
||
But how do we detect, if it's the 1st or 2nd call? The `$this->exactly(2)` expression actually returns a value object `PHPUnit\Framework\MockObject\Rule\InvokedCount` that we can work with. | ||
|
||
```php | ||
$invokedCount = $this->exactly(2); | ||
|
||
$mock = $this->createMock(MyClass::class); | ||
$mock->expects($invokedCount) | ||
->method('someMethod') | ||
->withCallable(function ($parameters) use ($invokedCount) { | ||
// check the parameters here | ||
}); | ||
``` | ||
|
||
On every method mock invoke, the number of invokes in `$invokedCount` will get increased. | ||
|
||
<br> | ||
|
||
We can use it to detect the 1st or 2nd call: | ||
|
||
```php | ||
$invokedCount = $this->exactly(2); | ||
|
||
$mock = $this->createMock(MyClass::class); | ||
$mock->expects($invokedCount) | ||
->method('someMethod') | ||
->withCallable(function ($parameters) use ($invokedCount) { | ||
if ($invokedCount->getInvocationCount() === 1) { | ||
// check the 1st round here | ||
} | ||
|
||
if ($invokedCount->getInvocationCount() === 2) { | ||
// check the 2nd round here | ||
} | ||
}); | ||
``` | ||
|
||
Now we include original parameters we needed it: | ||
|
||
```php | ||
// ... | ||
|
||
->withCallable(function ($parameters) use ($invokedCount) { | ||
if ($invokedCount->getInvocationCount() === 1) { | ||
$this->assertSame(['first'], $parameters); | ||
} | ||
|
||
if ($invokedCount->getInvocationCount() === 2) { | ||
$this->assertSame(['second'], $parameters); | ||
} | ||
}); | ||
``` | ||
|
||
Now this where this deprecation becomes useful. What if one of parameters is an product object? | ||
|
||
We could create a `$product` object and do `assertSame()`. But what if we only care about its price? | ||
|
||
```php | ||
// ... | ||
->withCallable(function ($parameters) use ($invokedCount) { | ||
if ($invokedCount->getInvocationCount() === 1) { | ||
$product = $parameter[0]; | ||
$this->assertInstanceof(Product::class, $product); | ||
$this->assertSame(100, $product->getPrice()); | ||
} | ||
|
||
// ... | ||
}); | ||
``` | ||
|
||
This would turn into single-line mess using `withConsecutive()`. Now it's more readable and flexible. | ||
|
||
<br> | ||
|
||
## Why `if` over `match`? | ||
|
||
Originally, we used `match()` expression over `ifs()` in Rector rule, but it created couple of new problems: | ||
|
||
* PHPUnit 9.x requires PHP 7.3+. Using `match()` would mean you have to do the upgrade to PHP 8 and to PHPUnit 10 at the same time. This is not always possible and can be risky | ||
* The call count is already checked by `$this->exactly(2)`. There is no need to add another layer of complexity to check the same thing again | ||
* With `match()` there is only single line of expression. Assert above would be single line: | ||
|
||
```php | ||
=> $product = $parameters[0] && $this->assertInstanceof(Product::class, $product) && $this->assertSame(100, $product->getPrice()) | ||
``` | ||
|
||
Which is not readable and maintainable. There is also one more reason why `if()` is the king. | ||
|
||
|
||
## Return value | ||
|
||
More often than not, the method not only accepts parameters, but also return some value. That's where `willReturn*()` methods come into play: | ||
|
||
```php | ||
$mock = $this->createMock(MyClass::class); | ||
$mock->expects($this->exactly(2)) | ||
->method('someMethod') | ||
->withConsecutive( | ||
['first'], | ||
['second'] | ||
) | ||
->willReturnOnConsecutiveCalls([1, 2]); | ||
``` | ||
|
||
How do we upgrade any returned value? We just return it: | ||
|
||
```php | ||
// ... | ||
|
||
->withCallable(function ($parameters) use ($invokedCount) { | ||
if ($invokedCount->getInvocationCount() === 1) { | ||
$this->assertSame(['first'], $parameters); | ||
return 1; | ||
} | ||
|
||
if ($invokedCount->getInvocationCount() === 2) { | ||
$this->assertSame(['second'], $parameters); | ||
return 2; | ||
} | ||
}); | ||
``` | ||
|
||
That's it! How about other return methods? | ||
|
||
```php | ||
->willReturnArgument(0); | ||
// or | ||
->willReturnSelf(); | ||
// or | ||
->willThrowException(new \Exception('Never happens')); | ||
``` | ||
|
||
We can just write plain PHP code: | ||
|
||
```php | ||
// ... | ||
|
||
->withCallable(function ($parameters) use ($invokedCount) { | ||
if ($invokedCount->getInvocationCount() === 1) { | ||
return $parameters[0]; | ||
// or | ||
return $this->userServiceMock; | ||
// or | ||
throw new Exception('Never happens'); | ||
} | ||
}); | ||
``` | ||
|
||
## More Readable and Easier to Maintain | ||
|
||
* We don't have to learn special PHPUnit mock method naming and we can understand the code. | ||
* This vanilla PHP also opens up next step - refactoring [away from mocks to anonymous typed classes](/blog/2018/06/11/how-to-turn-mocks-from-nightmare-to-solid-kiss-tests) | ||
* We can easily add new assertion line | ||
* We can return values we need | ||
|
||
|
||
What if in upcoming PHPUnit 12, 13, 14... versions some of mocking methods will be changed or removed? This code will most likely work, as it's just plain PHP. | ||
|
||
<br> | ||
|
||
This is how we can upgrade `withConsecutive()` method in PHPUnit 9 or earlier. I hope it's more clear now why this change was needed, and how it can help you write better tests. | ||
|
||
<br> | ||
|
||
Last but not least, here is [the Rector rule](https://getrector.com/rule-detail/with-consecutive-rector) that automated this process. | ||
|
||
<br> | ||
|
||
|
||
Happy coding! |