Back to Phpstan

Testing

website/src/developing-extensions/testing.md

2.1.543.6 KB
Original Source

Your custom extensions should have unit tests in order to prevent bugs sneaking into your code.

PHPStan supports testing with the industry standard PHPUnit framework.

Custom rules

Custom rules can be tested in a test case extended from PHPStan\Testing\RuleTestCase. A typical test can look like this:

php
<?php declare(strict_types = 1);

namespace App\PHPStan;

use PHPStan\Testing\RuleTestCase;

/**
 * @extends RuleTestCase<MyRule>
 */
class MyRuleTest extends RuleTestCase
{

	protected function getRule(): \PHPStan\Rules\Rule
	{
		// getRule() method needs to return an instance of the tested rule
		return new MyRule();
	}

	public function testRule(): void
	{
		// first argument: path to the example file that contains some errors that should be reported by MyRule
		// second argument: an array of expected errors,
		// each error consists of the asserted error message, and the asserted error file line
		$this->analyse([__DIR__ . '/data/my-rule.php'], [
			[
				'X should not be Y', // asserted error message
				15, // asserted error line
			],
		]);

		// the test fails, if the expected error does not occur,
		// or if there are other errors reported beside the expected one
	}

	public static function getAdditionalConfigFiles(): array
	{
		// path to your project's phpstan.neon, or extension.neon in case of custom extension packages
		// this is only necessary if your custom rule relies on some extra configuration and other extensions
		return [__DIR__ . '/../extension.neon'];
	}

}

Type inference

Other extension types, like dynamic return type extensions or type-specifying extensions, do not report errors by themselves, but rather influence the inferred types by the analysis engine so that PHPStan understands the analysed code better, remove false-positives1, and narrow down the types to report more useful errors.

A typical type inference test case class might look like this:

php
<?php declare(strict_types = 1);

namespace App\PHPStan;

use PHPStan\Testing\TypeInferenceTestCase;

class MyContainerDynamicReturnTypeExtensionTest extends TypeInferenceTestCase
{

	/**
	 * @return iterable<mixed>
	 */
	public static function dataFileAsserts(): iterable
	{
		// path to a file with actual asserts of expected types:
		yield from self::gatherAssertTypes(__DIR__ . '/data/my-container-types.php');
	}

	/**
	 * @dataProvider dataFileAsserts
	 */
	public function testFileAsserts(
		string $assertType,
		string $file,
		mixed ...$args
	): void
	{
		$this->assertFileAsserts($assertType, $file, ...$args);
	}

	public static function getAdditionalConfigFiles(): array
	{
		// path to your project's phpstan.neon, or extension.neon in case of custom extension packages
		return [__DIR__ . '/../extension.neon'];
	}

}

And the referenced file looks like normal analysed code, but takes advantage of the \PHPStan\Testing\assertType() function with the type assertions:

php
<?php

namespace MyTypesTest;

use function PHPStan\Testing\assertType;

class Foo
{

	public function doFoo(\App\MyContainer $container): void
	{
		// arguments: string with the expected type, actual type
		assertType(FooService::class, $container->getService('foo'));
		assertType(BarService::class, $container->getService('bar'));

		assertType('bool', $container->getParameter('isProduction'));
	}

}

Footnotes

  1. An error that's reported but isn't actually a bug in the analysed code.