Still on PHPUnit 4? Come to PHPUnit 8 Together in a Day
This post was updated in November 2020 with fresh know-how.
What is new?
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.
1. Planning
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:
- upgrade PHP 5.3 → 5.6
- upgrade PHPUnit 4 → 5
- upgrade PHP 5.6 → 7.0
- upgrade PHPUnit 5 → 6
- upgrade PHP 7.0 → 7.1
- upgrade PHPUnit 6 → 7
- upgrade PHP 7.1 → 7.2
- upgrade PHPUnit 7 → 8
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.
2. Single Version Upgrade = 1 pull-request
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".
The Golden Rule of Successful Upgrade
- one minor version at a time
- one pull-request at a time
- tested, our CI passes
- only after the PR is merged, we go to next minor version
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.
3. PHPUnit 4 to 5
From 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
4. PHPUnit 5 to 6
This is the release where PHPUnit got namespaces, yay! Well, yay for the project, nay for the upgrades.
From Underscore to Slash
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.
Add 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
5. PHPUnit 6 to 7
Remove "test" prefix on Data Providers
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'];
}
}
Rename @scenario annotation to @test
class WithTestAnnotation extends \PHPUnit\Framework\TestCase
{
/**
- * @scenario
+ * @test
*/
public function test()
{
// ...
}
}
Change withConsecutive() Arguments to Iterable
This 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
6. PHPUnit 7 to 8
Ignore Cache File
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
Remove type $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 = '')
{
}
}
Replace 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');
}
}
Replace assertInternalType() with Specific Methods
This 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);
}
}
Change 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');
}
}
Last _ is now Namespaced
This removes the last piece of back-compatible underscore class:
-PHPUnit_Framework_MockObject_MockObject
+PHPUnit\Framework\MockObject\MockObject
Replace assertArraySubset() with
This 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
Add void to PHPUnit\Framework\TestCase Methods
This 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!