Skip to content

Commit 82c0ff2

Browse files
fancywebnicolas-grekas
authored andcommitted
[PhpUnitBridge] Add the ability to expect a deprecation inside a test
1 parent 0d8ea5a commit 82c0ff2

File tree

4 files changed

+139
-22
lines changed

4 files changed

+139
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* ignore verbosity settings when the build fails because of deprecations
88
* added per-group verbosity
9+
* added `ExpectDeprecationTrait` to be able to define an expected deprecation from inside a test
910

1011
5.0.0
1112
-----

ExpectDeprecationTrait.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\PhpUnit;
13+
14+
use Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait;
15+
16+
trait ExpectDeprecationTrait
17+
{
18+
/**
19+
* @param string $message
20+
*
21+
* @return void
22+
*/
23+
protected function expectDeprecation($message)
24+
{
25+
if (!SymfonyTestsListenerTrait::$previousErrorHandler) {
26+
SymfonyTestsListenerTrait::$previousErrorHandler = set_error_handler([SymfonyTestsListenerTrait::class, 'handleError']);
27+
}
28+
29+
SymfonyTestsListenerTrait::$expectedDeprecations[] = $message;
30+
}
31+
}

Legacy/SymfonyTestsListenerTrait.php

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313

1414
use Doctrine\Common\Annotations\AnnotationRegistry;
1515
use PHPUnit\Framework\AssertionFailedError;
16+
use PHPUnit\Framework\RiskyTestError;
1617
use PHPUnit\Framework\TestCase;
1718
use PHPUnit\Framework\TestSuite;
1819
use PHPUnit\Runner\BaseTestRunner;
1920
use PHPUnit\Util\Blacklist;
2021
use PHPUnit\Util\Test;
2122
use Symfony\Bridge\PhpUnit\ClockMock;
2223
use Symfony\Bridge\PhpUnit\DnsMock;
24+
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
2325
use Symfony\Component\Debug\DebugClassLoader as LegacyDebugClassLoader;
2426
use Symfony\Component\ErrorHandler\DebugClassLoader;
2527

@@ -32,16 +34,16 @@
3234
*/
3335
class SymfonyTestsListenerTrait
3436
{
37+
public static $expectedDeprecations = [];
38+
public static $previousErrorHandler;
39+
private static $gatheredDeprecations = [];
3540
private static $globallyEnabled = false;
3641
private $state = -1;
3742
private $skippedFile = false;
3843
private $wasSkipped = [];
3944
private $isSkipped = [];
40-
private $expectedDeprecations = [];
41-
private $gatheredDeprecations = [];
42-
private $previousErrorHandler;
43-
private $error;
4445
private $runsInSeparateProcess = false;
46+
private $checkNumAssertions = false;
4547

4648
/**
4749
* @param array $mockedNamespaces List of namespaces, indexed by mocked features (time-sensitive or dns-sensitive)
@@ -220,15 +222,17 @@ public function startTest($test)
220222
if (isset($annotations['class']['expectedDeprecation'])) {
221223
$test->getTestResultObject()->addError($test, new AssertionFailedError('`@expectedDeprecation` annotations are not allowed at the class level.'), 0);
222224
}
223-
if (isset($annotations['method']['expectedDeprecation'])) {
224-
if (!\in_array('legacy', $groups, true)) {
225-
$this->error = new AssertionFailedError('Only tests with the `@group legacy` annotation can have `@expectedDeprecation`.');
225+
if (isset($annotations['method']['expectedDeprecation']) || $this->checkNumAssertions = \in_array(ExpectDeprecationTrait::class, class_uses($test), true)) {
226+
if (isset($annotations['method']['expectedDeprecation'])) {
227+
self::$expectedDeprecations = $annotations['method']['expectedDeprecation'];
228+
self::$previousErrorHandler = set_error_handler([self::class, 'handleError']);
226229
}
227230

228-
$test->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything(false);
231+
if ($this->checkNumAssertions) {
232+
$this->checkNumAssertions = $test->getTestResultObject()->isStrictAboutTestsThatDoNotTestAnything() && !$test->doesNotPerformAssertions();
233+
}
229234

230-
$this->expectedDeprecations = $annotations['method']['expectedDeprecation'];
231-
$this->previousErrorHandler = set_error_handler([$this, 'handleError']);
235+
$test->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything(false);
232236
}
233237
}
234238
}
@@ -242,9 +246,12 @@ public function endTest($test, $time)
242246
$className = \get_class($test);
243247
$groups = Test::getGroups($className, $test->getName(false));
244248

245-
if ($errored = null !== $this->error) {
246-
$test->getTestResultObject()->addError($test, $this->error, 0);
247-
$this->error = null;
249+
if ($this->checkNumAssertions) {
250+
if (!self::$expectedDeprecations && !$test->getNumAssertions()) {
251+
$test->getTestResultObject()->addFailure($test, new RiskyTestError('This test did not perform any assertions'), $time);
252+
}
253+
254+
$this->checkNumAssertions = false;
248255
}
249256

250257
if ($this->runsInSeparateProcess) {
@@ -263,24 +270,26 @@ public function endTest($test, $time)
263270
$this->runsInSeparateProcess = false;
264271
}
265272

266-
if ($this->expectedDeprecations) {
273+
if (self::$expectedDeprecations) {
267274
if (!\in_array($test->getStatus(), [BaseTestRunner::STATUS_SKIPPED, BaseTestRunner::STATUS_INCOMPLETE], true)) {
268-
$test->addToAssertionCount(\count($this->expectedDeprecations));
275+
$test->addToAssertionCount(\count(self::$expectedDeprecations));
269276
}
270277

271278
restore_error_handler();
272279

273-
if (!$errored && !\in_array($test->getStatus(), [BaseTestRunner::STATUS_SKIPPED, BaseTestRunner::STATUS_INCOMPLETE, BaseTestRunner::STATUS_FAILURE, BaseTestRunner::STATUS_ERROR], true)) {
280+
if (!\in_array('legacy', $groups, true)) {
281+
$test->getTestResultObject()->addError($test, new AssertionFailedError('Only tests with the `@group legacy` annotation can expect a deprecation.'), 0);
282+
} elseif (!\in_array($test->getStatus(), [BaseTestRunner::STATUS_SKIPPED, BaseTestRunner::STATUS_INCOMPLETE, BaseTestRunner::STATUS_FAILURE, BaseTestRunner::STATUS_ERROR], true)) {
274283
try {
275284
$prefix = "@expectedDeprecation:\n";
276-
$test->assertStringMatchesFormat($prefix.'%A '.implode("\n%A ", $this->expectedDeprecations)."\n%A", $prefix.' '.implode("\n ", $this->gatheredDeprecations)."\n");
285+
$test->assertStringMatchesFormat($prefix.'%A '.implode("\n%A ", self::$expectedDeprecations)."\n%A", $prefix.' '.implode("\n ", self::$gatheredDeprecations)."\n");
277286
} catch (AssertionFailedError $e) {
278287
$test->getTestResultObject()->addFailure($test, $e, $time);
279288
}
280289
}
281290

282-
$this->expectedDeprecations = $this->gatheredDeprecations = [];
283-
$this->previousErrorHandler = null;
291+
self::$expectedDeprecations = self::$gatheredDeprecations = [];
292+
self::$previousErrorHandler = null;
284293
}
285294
if (!$this->runsInSeparateProcess && -2 < $this->state && ($test instanceof \PHPUnit\Framework\TestCase || $test instanceof TestCase)) {
286295
if (\in_array('time-sensitive', $groups, true)) {
@@ -292,10 +301,10 @@ public function endTest($test, $time)
292301
}
293302
}
294303

295-
public function handleError($type, $msg, $file, $line, $context = [])
304+
public static function handleError($type, $msg, $file, $line, $context = [])
296305
{
297306
if (E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) {
298-
$h = $this->previousErrorHandler;
307+
$h = self::$previousErrorHandler;
299308

300309
return $h ? $h($type, $msg, $file, $line, $context) : false;
301310
}
@@ -308,7 +317,7 @@ public function handleError($type, $msg, $file, $line, $context = [])
308317
if (error_reporting()) {
309318
$msg = 'Unsilenced deprecation: '.$msg;
310319
}
311-
$this->gatheredDeprecations[] = $msg;
320+
self::$gatheredDeprecations[] = $msg;
312321

313322
return null;
314323
}

Tests/ExpectDeprecationTraitTest.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bridge\PhpUnit\Tests;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
16+
17+
final class ExpectDeprecationTraitTest extends TestCase
18+
{
19+
use ExpectDeprecationTrait;
20+
21+
/**
22+
* Do not remove this test in the next major version.
23+
*
24+
* @group legacy
25+
*/
26+
public function testOne()
27+
{
28+
$this->expectDeprecation('foo');
29+
@trigger_error('foo', E_USER_DEPRECATED);
30+
}
31+
32+
/**
33+
* Do not remove this test in the next major version.
34+
*
35+
* @group legacy
36+
*/
37+
public function testMany()
38+
{
39+
$this->expectDeprecation('foo');
40+
$this->expectDeprecation('bar');
41+
@trigger_error('foo', E_USER_DEPRECATED);
42+
@trigger_error('bar', E_USER_DEPRECATED);
43+
}
44+
45+
/**
46+
* Do not remove this test in the next major version.
47+
*
48+
* @group legacy
49+
*
50+
* @expectedDeprecation foo
51+
*/
52+
public function testOneWithAnnotation()
53+
{
54+
$this->expectDeprecation('bar');
55+
@trigger_error('foo', E_USER_DEPRECATED);
56+
@trigger_error('bar', E_USER_DEPRECATED);
57+
}
58+
59+
/**
60+
* Do not remove this test in the next major version.
61+
*
62+
* @group legacy
63+
*
64+
* @expectedDeprecation foo
65+
* @expectedDeprecation bar
66+
*/
67+
public function testManyWithAnnotation()
68+
{
69+
$this->expectDeprecation('ccc');
70+
$this->expectDeprecation('fcy');
71+
@trigger_error('foo', E_USER_DEPRECATED);
72+
@trigger_error('bar', E_USER_DEPRECATED);
73+
@trigger_error('ccc', E_USER_DEPRECATED);
74+
@trigger_error('fcy', E_USER_DEPRECATED);
75+
}
76+
}

0 commit comments

Comments
 (0)