Switch from deprecated --set
option to rector.php
config.
Last month I was on PHPSW meetup in Bristol UK with Rector talk. To be honest, Nette to Symfony migration under 80 hours was not a big deal there.
To my surprise, upgrading PHPUnit tests was. So I was thinking, let's take it from the floor in one go, from PHPUnit 4 to the latest PHPUnit 8.
Before we dive into the upgrading of our tests, we need to look at minimal PHP version required by each PHPUnit. The PHPUnit release process states, that each new PHPUnit major version requires a newer minor PHP version.
What does that mean?
PHPUnit | Required PHP version | Release Year |
---|---|---|
PHPUnit 4 | PHP 5.3-5.6 | 2015 |
PHPUnit 5 | PHP 5.6-7.4 | 2016 |
PHPUnit 6 | PHP 7.0-7.4 | 2017 |
PHPUnit 7 | PHP 7.1-7.4 | 2018 |
PHPUnit 8 | PHP 7.2-7.4 | 2019 |
We need to plan and combine the PHP upgrade.
This is the full path we'll go through:
Note: To keep this post simple, we'll focus on PHPUnit upgrade only here. But it's possible you'll need to use Rector for PHP upgrades between PHPUnit upgrade steps too.
Do you enjoy the rush of changing a thousand files at once? It drives to do even more and more changes.
The same thing happens with upgrading code - we upgrade PHP and tests passes, great! Let's do upgrade PHPUnit. Oh, maybe we could refactor this method so it's more readable. Oh, now this other method looks silly, let's clean that up too...
STOP!This is the easiest way to get stuck in broken tests, with dug up pieces of code all over the project, overheat our brain in huge cognitive complexity and give up rage quit the upgrade saying "it's hard as they say".
Unless you're upgrading 10 test files, of course. But in the rest of the case, this approach would save me many failed attempts in the paths. That's why planning is the most important step of all for huge upgrade operations.
getMock()
to getMockBuilder()
final class MyTest extends PHPUnit_Framework_TestCase
{
public function test()
{
- $someClassMock = $this->getMock('SomeClass');
+ $someClassMock = $this->getMockBuilder('SomeClass')->getMock();
}
}
These changes can be delegated to Rector:
composer require phpunit/phpunit "^5.0" --dev
Update set in rector.php
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->import(PHPUnitSetList::PHPUNIT_50);
};
Run Rector
vendor/bin/rector process tests
This is the release where PHPUnit got namespaces, yay! Well, yay for the project, nay for the upgrades.
Although there is a lot of underscore <=> slash aliases for a smoother upgrade, it will be removed in the future PHPUnit version, so we better deal with it right now.
First, we need to replace all:
-PHPUnit_Framework_TestCase
+\PHPUnit\Framework\TestCase # mind the pre-slash "\"
Without pre-slash, your code might fail.
See 3v4l.org to understand whySecond, replace the rest of the PHPUnit_*
classes with a namespace. Listeners, test suites, exceptions... etc.
This is hell for us human, luckily easy-pick for Rector.
doesNotPerformAssertions
to test With no Assertion+ /**
+ * @doesNotPerformAssertions
+ */
public function testNoError()
{
new SomeObject(25);
}
setExpectedException()
to expectException()
And not only that! Also, split arguments to own method:
<?php
class MyTest extends \PHPUnit\Framework\TestCase
{
public function test()
{
- $this->setExpectedException('SomeException', $message, 101);
+ $this->expectException('SomeException');
+ $this->expectExceptionMessage($message);
+ $this->expectExceptionCode(101);
}
}
Also setExpectedExceptionRegExp()
was removed.
@expectedException
to expectException()
These changes are a real challenge for simple human attention:
-expectedException
+expectException
Mind the missing "ed".
<?php
class MyTest extends \PHPUnit\Framework\TestCase
{
- /**
- * @expectedException SomeException
- */
public function test()
{
+ $this->expectException('SomeException');
}
}
These changes can be delegated to Rector:
composer require phpunit/phpunit "^6.0" --dev
Update set in rector.php
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->import(PHPUnitSetList::PHPUNIT_60);
};
Run Rector
vendor/bin/rector process tests
I have no idea how tests and data provider methods were detected before this:
class WithTestAnnotation extends \PHPUnit\Framework\TestCase
{
/**
- * @dataProvider testProvideDataForWithATestAnnotation()
+ * @dataProvider provideDataForWithATestAnnotation()
*/
public function test()
{
// ...
}
- public function testProvideDataForWithATestAnnotation()
+ public function provideDataForWithATestAnnotation()
{
return ['123'];
}
}
@scenario
annotation to @test
class WithTestAnnotation extends \PHPUnit\Framework\TestCase
{
/**
- * @scenario
+ * @test
*/
public function test()
{
// ...
}
}
withConsecutive()
Arguments to IterableThis rather small change can cause a huge headache. It's a fix of silent false positive.
How would you fix the following code, if you know that the argument of withConsecutive()
must be iterable (array, iterator...)?
class SomeClass
{
public function run($one, $two)
{
}
}
class SomeTestCase extends \PHPUnit\Framework\TestCase
{
public function test()
{
$someClassMock = $this->createMock(SomeClass::class);
$someClassMock
->expects($this->exactly(2))
->method('run')
->withConsecutive(1, 2, 3, 5);
}
}
Like this?
-->withConsecutive(1, 2, 3, 5);
+->withConsecutive([1, 2, 3, 5]);
Well, the tests would pass it, but it would be another silent positive. Look at SomeClass::run()
method. How many arguments does it have?
Two. So we need to create array chunks of size 2.
-->withConsecutive(1, 2, 3, 5);
+->withConsecutive([1, 2], [3, 5]);
These changes can be delegated to Rector:
composer require phpunit/phpunit "^7.0" --dev
Update set in rector.php
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->import(PHPUnitSetList::PHPUNIT_70);
$rectorConfig->import(PHPUnitSetList::PHPUNIT_75);
};
Run Rector
vendor/bin/rector process tests
The optional caching of test results added in PHPUnit 7.3 is not enabled by default. We need to add the cache file to .gitignore
:
# .gitignore
.phpunit.result.cache
$dataName
in PHPUnit\Framework\TestCase
Constructor Override <?php
abstract class MyAbstractTestCase extends \PHPUnit\Framework\TestCase
{
- public function __construct(?string $name = null, array $data = [], string $dataName = '')
+ public function __construct(?string $name = null, array $data = [], $dataName = '')
{
}
}
assertContains()
with Specific assertStringContainsString()
Method <?php
final class SomeTest extends \PHPUnit\Framework\TestCase
{
public function test()
{
- $this->assertContains('foo', 'foo bar');
+ $this->assertStringContainsString('foo', 'foo bar');
- $this->assertNotContains('foo', 'foo bar');
+ $this->assertStringNotContainsString('foo', 'foo bar');
}
}
assertInternalType()
with Specific MethodsThis change is huge.
2 methods were removed, 22 methods are added.
<?php
final class SomeTest extends \PHPUnit\Framework\TestCase
{
public function test()
{
- $this->assertInternalType('string', $value);
+ $this->assertIsString($value);
}
}
assertEquals()
method Parameters to new Specific Methods final class SomeTest extends \PHPUnit\Framework\TestCase
{
public function test()
{
$value = 'value';
- $this->assertEquals('string', $value, 'message', 5.0);
+ $this->assertEqualsWithDelta('string', $value, 5.0, 'message');
- $this->assertEquals('string', $value, 'message', 0.0, 20);
+ $this->assertEquals('string', $value, 'message', 0.0);
- $this->assertEquals('string', $value, 'message', 0.0, 10, true);
+ $this->assertEqualsCanonicalizing('string', $value, 'message');
- $this->assertEquals('string', $value, 'message', 0.0, 10, false, true);
+ $this->assertEqualsIgnoringCase('string', $value, 'message');
}
}
_
is now NamespacedThis removes the last piece of back-compatible underscore class:
-PHPUnit_Framework_MockObject_MockObject
+PHPUnit\Framework\MockObject\MockObject
assertArraySubset()
withThis method was removed because of its vague behavior. The proposed solution is rdohms/phpunit-arraysubset-asserts polyfill.
namespace Acme\Tests;
+use DMS\PHPUnitExtensions\ArraySubset\Assert;
final class AssertTest extends \PHPUnit\Framework\TestCase
{
public function testPreviouslyStaticCall(): void
{
- $this->assertArraySubset(['bar' => 0], ['bar' => '0'], true);
+ Assert::assertArraySubset(['bar' => 0], ['bar' => '0'], true);
}
}
To use this package and upgrade to it, run:
composer require --dev dms/phpunit-arraysubset-asserts
Update set in rector.php
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->import(PHPUnitSetList::PHPUNIT80_DMS);
};
Run Rector
vendor/bin/rector process tests
void
to PHPUnit\Framework\TestCase
MethodsThis one hits 2 common methods we often use:
-setUp()
+setUp(): void
-tearDown()
+tearDown(): void
Also less common ones:
setUpBeforeClass()
assertPreConditions()
assertPostConditions()
tearDownAfterClass()
onNotSuccessfulTest()
For this one, we'll use little help from Symplify:
composer require symplify/phpunit-upgrader --dev
vendor/bin/phpunit-upgrader voids /tests
That's it!
Then back to Rector - Update set in rector.php
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Config\RectorConfig;
return function (RectorConfig $rectorConfig): void {
$rectorConfig->import(PHPUnitSetList::PHPUNIT_80);
};
Then:
composer require phpunit/phpunit "^8.0" --dev
In the end, you should see at least PHPUnit 8.5+:
vendor/bin/phpunit --version
$ PHPUnit 8.5.15 by Sebastian Bergmann and contributors.
That's it! Congrats!
Did you find a change that we missed here? Share it in comments, so we can make this upgrade path complete and smooth for all future readers. Thank you for the whole PHP community!
Happy coding!
Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!