Open-source project
phpstan/phpstan-phpunit avatar
phpstan/phpstan-phpunit

phpstan-phpunit, the extension that teaches PHPStan what a PHPUnit mock really is

PHPUnit extensions and rules for PHPStan

546 stars58 forksPHPMIT

At a glance

What is it?
phpstan/phpstan-phpunit makes PHPStan understand PHPUnit test code: mock factories return an intersection of the double and the mocked class, assertion arguments get their types, and a separate optional rules file flags assertions that say less than they appear to. The mock typing is worth adopting immediately, the strict rules are a migration you schedule.
Who is it for?
phpstan-phpunit suits a PHP project that already runs PHPStan over a PHPUnit suite and is tired of false positives around mocks, since including extension.neon is a configuration change that fixes the type modelling without touching a line of test code. It does not suit a project without PHPStan, a team that wants the strict assertion rules without a cleanup budget, or a codebase that builds assertions dynamically.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 10 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

PHPStan extensions that teach it what a PHPUnit mock is

phpstan/phpstan-phpunit is the bridge between two tools that do not understand each other's vocabulary. PHPStan does not know that createMock returns something you can call expects() on, and PHPUnit's own type hints do not say whether a given value is a double of the class you asked for. This extension resolves that, and the documentation lists the features it adds.

The core feature is that createMock(), getMockForAbstractClass() and getMockFromWsdl() return an intersection type of the mock object and the mocked class. So the value has both the methods of the mock object, such as expects, and the methods of the class you asked to be mocked. The same support covers getMock() called on a MockBuilder. A second feature interprets Foo|MockObject written in phpDoc as an intersection type rather than a union, so old annotations that used the pipe syntax get the newer and correct meaning.

Then there is the part that stops false positives. The extension defines early terminating method calls for the PHPUnit\Framework\TestCase class, so a failing assertion does not produce undefined variable errors afterwards. It specifies the types of expressions passed to assertion methods such as assertInstanceOf, assertTrue and assertInternalType. And combined with PHPStan's level 4, it points out always-true and always-false assertions like assertTrue(true), which is exactly the kind of test that looks like coverage and asserts nothing.

The intended user is a PHP project that runs PHPStan over a PHPUnit suite and wants the analysis to be correct about test code rather than merely tolerable.

Strict rules that argue with your assertions

Beyond the type modelling, the extension ships a set of framework specific rules that can be enabled separately, and their reasoning is about intent rather than types.

Four of them are about choosing the right assertion for the value. Using assertSame() with true as the expected value is flagged, with assertTrue() as the alternative. The same applies to false, where assertFalse() is the alternative, and to null, where assertNull() is the alternative. A fourth checks for assertSame() with count($variable) as the second parameter, where assertCount($variable) should be used instead.

The reasoning is that assertSame() expresses identity with a specific value, and when the value is a boolean or null the assertion loses the information about which value you meant. It also means a test that intends to check truthiness but is written as assertSame(true, $x) will fail on a truthy but non-boolean value, which is often a bug in the test rather than the code.

Two more rules compare assertion pairs. assertEquals() with same types is flagged, with assertSame() as the alternative, and assertNotEquals() with same types is flagged, with assertNotSame() as the alternative. The point is that loose comparison between two values of the same type is usually just a slower and vaguer version of strict comparison.

And one rule is conditional. If PHPStan Strict Rules are enabled, PHPUnit's assertEmpty() and assertNotEmpty() assertions are disallowed as well, because an empty check is too permissive to express what a test usually means.

These rules are opt in, which is the right default for a project that has an existing suite, because enabling them produces a wall of findings on code that was previously fine.

Typing a mock so both the double and the class are visible

The documentation spends most of its space on a practical question: how do you write a type for a mock you keep around, and what happens if you cannot use native intersection types yet.

The intersection type is written as Foo&\PHPUnit\Framework\MockObject\MockObject, with an ampersand. The documentation is explicit that the pipe syntax Foo|\PHPUnit\Framework\MockObject\MockObject is also supported, but only for ecosystem and legacy reasons. Here is the native form, with a factory method that returns the intersection and a test that then configures the double:

php
private Foo $foo;

protected function setUp(): void
{
	$fooMock = $this->createMock(Foo::class);
	$fooMock->method('doFoo')->will($this->returnValue('test'));
	$this->foo = $fooMock;
}

public function testSomething(): void
{
	$this->foo->doFoo();
	// $this->foo->method() and expects() can no longer be called
}

php private function createFooMock(): Foo&\PHPUnit\Framework\MockObject\MockObject { return $this->createMock(Foo::class); }

public function testSomething(): void { $fooMock = $this->createFooMock(); $fooMock->method('doFoo')->will($this->returnValue('test')); $fooMock->doFoo(); }

code

If you cannot use native intersection types yet, the same effect is available through phpDoc, and the method itself keeps the simpler native return type:

php

php private Foo $foo;

protected function setUp(): void { $fooMock = $this->createMock(Foo::class); $fooMock->method('doFoo')->will($this->returnValue('test')); $this->foo = $fooMock; }

public function testSomething(): void { $this->foo->doFoo(); // $this->foo->method() and expects() can no longer be called }

code

The third case is the one that trips people up. If the mock is fully configured and only the methods of the mocked class are supposed to be called on the value, it is fine to typehint only the mocked class:

php

php private Foo $foo;

protected function setUp(): void { $fooMock = $this->createMock(Foo::class); $fooMock->method('doFoo')->will($this->returnValue('test')); $this->foo = $fooMock; }

public function testSomething(): void { $this->foo->doFoo(); // $this->foo->method() and expects() can no longer be called }

code

In that arrangement the value is stored in a property typed as the class, and the documentation notes in a comment that method() and expects() can no longer be called on it. That is the trade-off: the narrower type is honest about how the value is used, and it costs you the ability to reconfigure the double after the fact.

The pattern to copy is the first one. Configure the double at the point of creation, keep the intersection type on the return, and you can call either set of methods on it without the analyser complaining.

Installing the extension, with and without the extension installer

Installation is a Composer require as a development dependency:

bash

bash composer require --dev phpstan/phpstan-phpunit

code

If you also install phpstan/extension-installer, the documentation says you are all set. That is the easy path, because the installer picks up the extension automatically and there is nothing to wire into your configuration.

The manual path is documented in a collapsed section, for projects that do not want the extra package. You include extension.neon in your PHPStan config:

yaml

yaml includes: - vendor/phpstan/phpstan-phpunit/extension.neon

code

And if you want the framework specific checks described earlier, you include a second file as well:

yaml

yaml - vendor/phpstan/phpstan-phpunit/rules.neon

code

So there are two levels of adoption, and they are separate. The first includes the type modelling, where mocks become intersections and assertion arguments get their types. The second adds the strict rules that tell you assertSame() was the wrong assertion, and those are the ones that will produce findings across an existing test suite.

The repository layout backs this split. At the top level there is extension.neon and rules.neon as separate files, plus src/, stubs/, tests/ and an e2e/ directory for end to end cases. There is a phpstan.neon and a phpstan-baseline.neon for the project's own analysis, a phpunit.xml for its own tests, and a collision-detector.json used by a name-collision make target. A CLAUDE.md file sits alongside the rest, and there is a Makefile that wires the checks together.

The Makefile shows how the project checks itself

The Makefile is short and it is the clearest statement of what this repository considers a complete check.

The aggregate target is check, which depends on lint, cs, tests and phpstan. The lint target runs parallel-lint over src and tests with colours. The cs target runs phpcs with the build-cs standard over the same two directories, and there is a matching cs-fix target that runs phpcbf. The cs-install target clones phpstan/build-cs, resets it to the 2.x branch and runs composer install inside it, which is how the coding standard itself is obtained rather than vendored.

The tests target runs php vendor/bin/phpunit, and the phpstan target runs php vendor/bin/phpstan analyse -c phpstan.neon. There is a second PHPStan target, phpstan-generate-baseline, which runs the same analysis with the -b flag pointing at phpstan-baseline.neon. Having a committed baseline is standard practice, and it is worth noticing that this repository analyses itself with the extension it publishes, which is a reasonable self check for a package whose whole job is modelling PHPUnit types.

The name-collision target runs detect-collisions with collision-detector.json. For a package that ships stubs and rules that add types to framework classes, a collision detector is the mechanism that catches two extensions claiming the same symbol, which would otherwise produce confusing errors in a user's project.

The default branch is 2.1.x, and the release history is recent: 2.0.21 on 2026-10-01, 2.1.0 on 2026-10-06 and 2.1.1 on 2026-10-07. The last push was on 2026-10-13, and the licence is MIT.

Where this extension is the wrong tool

The first wrong case is a project that does not run PHPStan. This is an extension for a static analyser, so without PHPStan in the pipeline there is nothing for it to do, and the type modelling has no effect on your tests at runtime.

The second wrong case is a project on PHPUnit 10 or later that has already restructured its test code. The extension is written around the mock API of the PHPUnit versions it supports, and the documentation does not describe behaviour for the newest PHPUnit releases, so an upgrade of the test framework can change what the extension models.

The third wrong case is a codebase that wants type information but not stricter assertions. The strict rules and the type modelling are separable, and mixing them up is the most common way to get an unpleasant first experience. A team that only wants the mock intersection types should include extension.neon and leave rules.neon out.

The fourth wrong case is dynamic test suites that build assertions at runtime, where the type of an expression passed to an assert method cannot be determined statically. The extension specifies argument types for those methods, which means a dynamic call site is exactly where it can add noise rather than remove it.

The fifth wrong case is a project with a large existing test suite that expects the strict rules to be free. Turning on rules.neon on a codebase that has been using assertSame(true, $x) for years produces a list of findings that are individually trivial to fix and collectively tedious. The rules are worth having, but they are a migration, not a toggle.

This extension against handling mocks by hand

The realistic alternative is not another analyser plugin, it is writing your own stub classes or living with the false positives.

Hand written stubs mean every test double is a class you maintain. You write the method signatures, the return values, and you keep them in sync with the class under test as that class changes. When the class gains a method, the stub is wrong until you notice. It also means no expects() mechanism, so anything that depends on asserting how a call was made has to be expressed differently.

The extension takes the opposite route. It does not create any code, it teaches an analyser about code that already exists. The PHPUnit mock object is a real runtime thing with real methods, and the problem is only that the type system does not express the relationship between the double and the class it doubles. Intersecting MockObject with the mocked class is a way of saying that the value is both, and once the analyser knows that, it can check calls against either set of methods.

The difference in cost is stark. A stub class is code you write, review and maintain forever, and it drifts. The extension is a configuration change plus a dependency, and it stays correct as long as the framework types it models do not change shape.

There is a middle path that also works: typehint only the mocked class, as the documentation allows, and lose the ability to reconfigure the double after creation. That avoids the intersection type entirely at the price of a stricter contract on how mocks are used, and for a codebase that configures mocks inline it costs nothing at all.

Editorial conclusion

phpstan-phpunit suits a PHP project that already runs PHPStan over a PHPUnit suite and is tired of false positives around mocks, since including extension.neon is a configuration change that fixes the type modelling without touching a line of test code. It does not suit a project without PHPStan, a team that wants the strict assertion rules without a cleanup budget, or a codebase that builds assertions dynamically. Before you turn on rules.neon, include extension.neon alone and see what the type modelling alone reports, then decide whether the assertSame and assertEquals findings are worth fixing across your suite, and check the default branch 2.1.x against the PHPUnit version you run, since the extension documents the mock API of the versions it supports and says nothing about newer releases.

Frequently asked questions

How do I install phpstan/phpstan-phpunit?

Require it in Composer as a development dependency with composer require --dev phpstan/phpstan-phpunit. If you also install phpstan/extension-installer the documentation says you are all set, otherwise include vendor/phpstan/phpstan-phpunit/extension.neon in your PHPStan config.

How do I enable the strict PHPUnit rules?

The strict framework specific rules are enabled separately by including vendor/phpstan/phpstan-phpunit/rules.neon in your PHPStan configuration, in addition to extension.neon. They cover things like using assertTrue() instead of assertSame() with true, and assertCount() instead of assertSame() with count().

How do I type a PHPUnit mock in a return type?

The documentation uses the intersection type Foo&\PHPUnit\Framework\MockObject\MockObject, for example private function createFooMock(): Foo&\PHPUnit\Framework\MockObject\MockObject. If you cannot use native intersection types yet, the same effect is available through a phpDoc @return annotation.

What does the extension do about mocks in phpDoc?

It interprets Foo|MockObject written in phpDoc as an intersection type rather than a union type. The documentation notes that the pipe syntax is supported only for ecosystem and legacy reasons, while the correct syntax uses an ampersand.

Does phpstan/phpstan-phpunit change what my tests do at runtime?

No. It is a PHPStan extension, so it affects static analysis only: it teaches the analyser that mock factories return an intersection of the mock object and the mocked class, specifies types for assertion arguments, and can flag always-true or always-false assertions when used with PHPStan level 4.

Official sources

  1. Issues
  2. License: MIT
  3. phpstan/phpstan-phpunit on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/phpstan-phpstan-phpunit.svg)](https://hysenlabs.com/projects/phpstan-phpstan-phpunit)

Was this useful?