Skip to content

Commit

Permalink
gram
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasVotruba committed Oct 10, 2024
1 parent 02aa090 commit b5ccf8b
Showing 1 changed file with 23 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ 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.
It was not a very popular BC break. There is no 1:1 replacement. It can be combined with `willReturn*()` methods, which 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.
PHPUnit upgrades take 95 % of the time to upgrade this single method and 5 % for everything else.
In recent months, we've done a couple of project upgrades with Rector and learned a lot.
Today, I want to share bit of knowledge with you and explain, **why it's a change for better code**.
Today, I want to share some knowledge with you and explain why it's a change for better code.
---

What does `withConsecutive()` method actually do?
Expand All @@ -24,17 +24,17 @@ $mock->expects($this->exactly(2))
);
```

It defines what arguments are on the input, once the method mock is called. E.g. here:
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.
To be honest, I've never written such code myself, but so far, we've 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:
So how can we replace it? It would be very convenient if there would some kind of `withNthCall()` method:

```php
$mock = $this->createMock(MyClass::class);
Expand All @@ -49,7 +49,7 @@ But it's not.

## `withCallable()` to the Rescue

Instead we use `withCallable()` trick. This methods accepts the called parameters, that we can assert inside.
Instead, we use the `withCallable()` trick. This method accepts the called parameters, which we can assert inside.

```php
$mock = $this->createMock(MyClass::class);
Expand All @@ -60,7 +60,7 @@ $mock->expects($this->exactly(2))
});
```

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.
But how do we detect if it's the first or second 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);
Expand All @@ -73,7 +73,7 @@ $mock->expects($invokedCount)
});
```

On every method mock invoke, the number of invokes in `$invokedCount` will get increased.
On every mock invoke method, the number of invokes in `$invokedCount` will increase.

<br>

Expand All @@ -96,7 +96,7 @@ $mock->expects($invokedCount)
});
```

Now we include original parameters we needed it:
Now we include the original parameters we needed:

```php
// ...
Expand All @@ -112,7 +112,7 @@ Now we include original parameters we needed it:
});
```

Now this where this deprecation becomes useful. What if one of parameters is an product object?
Now, this is where this deprecation becomes useful. What if one of the parameters is a product object?

We could create a `$product` object and do `assertSame()`. But what if we only care about its price?

Expand All @@ -129,28 +129,28 @@ We could create a `$product` object and do `assertSame()`. But what if we only c
});
```

This would turn into single-line mess using `withConsecutive()`. Now it's more readable and flexible.
Using `withConsecutive()` would turn this into a single-line mess. 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:
Originally, we used the `match()` expression over `ifs()` in the Rector rule, but it created a 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:
* With `match()`, there is only a single line of expression. Assert above would be a 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.
This code 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:
More often than not, the method not only accepts parameters but also returns some value. That's where `willReturn*()` methods come into play:

```php
$mock = $this->createMock(MyClass::class);
Expand Down Expand Up @@ -209,17 +209,17 @@ We can just write plain PHP code:

## 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 don't have to learn special PHPUnit mock method naming and can understand the code.
* This vanilla PHP also opens up the 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 a 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.
What if, in the upcoming PHPUnit 12, 13, and 14... versions, remove or change more mocking methods? This code will 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.
This is how we can upgrade the `withConsecutive()` method in PHPUnit 9 or earlier. I hope it's clearer now why this change was needed and how it can help you write better tests.

<br>

Expand Down

0 comments on commit b5ccf8b

Please sign in to comment.