How to Migrate PhpSpec to PHPUnit

This post was updated at September 2023 with fresh know-how.
What is new?

Refresh with Rector 0.18 features. The Rector set is currently not available, but the rest of article is still valid and useful.


I'm happy that more and more people try to use Rector upgrade and migrate their code-bases to the ones they really want for a long time.

Last week I was approached by 2 different people with single need - migrate their tests to PHPUnit.

"Nobody believes in anything without an inner feeling that it can be realized.
This is the only source of dreamlike powers."
Peter Altenberg

Disclaimer: I never saw PhpSpec code before this mentoring session. All I learned was from my client and their needs (and bit of reading the documentation).

"Why do you Have 2 Unit-Testing Frameworks?"

That was my question to one of my clients when I saw both PhpSpec and PHPUnit tests in its code base.

But before I noticed PhpSpec and asked about it, we had another chat:

Then I explored PhpSpec and found out, it's basically PHPUnit with different naming.

"It looks like Y, a variant of X, could be done in
about half the time, and you lose only one feature."
The Pragmatic Programmer book

Trends over Long-Tail Effect

Why did we choose to migrate PhpSpec tests to PHPUnit? Well, it's better obviously... Do you agree with me just because I wrote that? Don't do that, it's my personal opinionated opinion (= feeling, emotion). Ask me for some data instead.

Let's look at downloads:

But 117 mil. downloads can be like "You should use Windows XP because it's the most used Windows version ever!" That's classic manipulation of dying dinosaur.

Let's see the trends! In the same order:

Which one would you pick from this 2 information? I'd go for the last one, so did my client. So that's why we agreed to migrate PhpSpec (the middle one) to PHPUnit.


This is how 1 spec migration might look like:

 <?php

-namespace spec\App\Product;
+namespace Tests\App\Product;

-use PhpSpec\ObjectBehavior;

-final class CategorySpec extends ObjectBehavior
+final class CategoryTest extends \PHPUnit\Framework\TestCase
 {
+    /**
+     * @var \App\Product\Category
+     */
+    private $createMe;

-    public function let()
+    protected function setUp()
     {
-        $this->beConstructedWith(5);
+        $this->createMe = new \App\Product\Category(5);
     }

-    public function it_returns_id()
+    public function testReturnsId()
     {
-        $this->id()->shouldReturn(5);
+        $this->assertSame(5, $this->createMe->id());
     }

-    public function it_blows()
+    public function testBlows()
     {
-        $this->shouldThrow('SomeException')->during('item', [5]);
+        $this->expectException('SomeException');
+        $this->createMe->item(5);
     }

-    public function it_should_be_called(Cart $cart)
+    public function testCalled()
     {
+        /** @var Cart|\PHPUnit\Framework\MockObject\MockObject $cart */
+        $cart = $this->createMock(Cart::class);
-        $cart->price()->shouldBeCalled()->willReturn(5);
+        $cart->expects($this->atLeastOnce())->method('price')->willReturn(5);
-        $cart->shippingAddress(Argument::type(Address::class))->shouldBeCalled();
+        $cart->expects($this->atLeastOnce())->method('shippingAddress')->with($this->isType(Address::class));
     }

-     public function is_bool_check()
+     public function testBoolCheck()
      {
-         $this->hasFailed()->shouldBe(false);
+         $this->assertFalse($this->createMe->hasFailed());
-         $this->hasFailed()->shouldNotBe(false);
+         $this->assertNotFalse($this->createMe->hasFailed());
      }

-     public function is_array_type()
+     public function testArrayType()
      {
-         $this->shippingAddresses()->shouldBeArray();
+         $this->assertIsIterable($this->createMe->shippingAddresses());
      }
 }

Pretty clear, right?

How to Migrate from PhpSpec to PHPUnit?

First, take a 2-week paid vacation... Just kidding. Start with Rector which migrates ~95 % of code cases. It also renames *Spec.php to *Test.php and moves them from /spec to /tests directory:

  1. Add Rector
composer require rector/rector --dev
  1. Create rector.php config
use Rector\Set\ValueObject\SetList;
use Rector\Config\RectorConfig;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->sets([SetList::PHPSPEC_TO_PHPUNIT]);
};
  1. Run Rector on your tests directories
vendor/bin/rector process tests

Take couple of minutes to polish the rest of code and send PR to your project ✅


Happy coding!




Do you learn from my contents or use open-source packages like Rector every day?
Consider supporting it on GitHub Sponsors. I'd really appreciate it!