DEV Community

Cover image for Test Doubles in PHP with Mockery and PHPUnit
Bedram Tamang
Bedram Tamang

Posted on • Originally published at blog.jobins.jp

Test Doubles in PHP with Mockery and PHPUnit

Test doubles is a technique in which you replace the real object by pretend object, or a real method by pretend method or just pre-define the return data and function arguments for a method to ease the testing purpose. In real world, our application may has various components which requires to connect different databases, APIs, and other services, which cannot be used in test environment. Instead of depending on networks, any other services or components, it is easier to use test doubles for testing purposes.

The term Test Double is first used by Gerard Meszaros on his book xUnit Test Patterns, to give a common name for stubs, mocks, fakes, dummies, and spies1. The name comes from the notion of a Stunt Double in movies1. Meszaros has defined five types of doubles which we will study in the perspective of PHPUnit & Mockery:

To illustrate the concept in details, Let's define a interface called GithubInterface as below,

interface GithubRepository
{
    public function find(string $uuid);

    public function get();

    public function create(RepositoryDTO $userDto);
}
Enter fullscreen mode Exit fullscreen mode

Here, GithubRepository interface has three methods, find(string $uuid) to find repositories by uuid, get() to get users repositories, and create(RepositoryDTO $userDto) to create a new repositories in Github.

  1. Stub: The idea of returning configured values in method call is called stub. In above example, the gets method expected to returns list of repositories in array format from remote github server. Instead of depending on network dependent service, we could stub the above get method just to define the return data for the get method as:
   /** @test */
   public function github_repository_get_method_with_stubs()
   {
       $stub = $this->createStub(GithubRepository::class);
       // using PHPUnit's createStub method to create stub

       $stub->method('get')
           ->willReturn([
               ['uuid' => 12345678, 'name' => 'laravel/laravel'],
               ['uuid' => 12345679, 'name' => 'laravel/framework']
           ]);

       $this->swap(GithubRepository::class, $stub);
       $this->assertTrue(is_array(app()->make(GithubRepository::class)->get()));   
    }
Enter fullscreen mode Exit fullscreen mode

In above example, a stub for GithubRepository::class class is created and which returns array of repositories. The swap method here is Laravel swap method, which binds the current stubs into Service Container.

  1. Spy & Mock: In definition, Spy & Mock are very similar to each others as Spy stores the interactions made on execution, and allows us to make assertions against those interactions, whereas Mock is a objects with pre-defined expectations.

Difference between Mock & Spy:

Mock Spy
Mock defines expectation at first and assert that after the interaction is happened. Spy stores interactions first and then it asserts against those interactions.
Expectation should be defined before any interaction happens. Doesn't care about expectation even if interactions are stored.
PHPUnit support Mock. Spy doesn't exists in PHPUnit.

PHPUnit Mock & Spy: PHPUnit doesn't support spy but support the Mock.

   /** @test */
   public function users_repository_find_method_with_spy()
   {
    $mock = $this->createMock(GithubRepository::class);

    $mock->expects($this->exactly(1))
        ->method('find')->with(1)
           ->willReturn(['uuid' => 12345678, 'name' => 'laravel/laravel']);

       $this->swap(GithubRepository::class, $mock);
       $this->assertTrue(is_array(app()->make(UserRepository::class)->find(1)));
   }
Enter fullscreen mode Exit fullscreen mode

Here, the PHPUnit's mock works quite differently than the definition. It works as combination of both spy and mock, in which the expectation is not required even it's mock, but can define expectation as shown in example above.

   $mock = $this->createMock(GithubRepository::class);
   $this->swap(GithubRepository::class, $mock);

   print_r(app()->make(GithubRepository::class)->find(1));
Enter fullscreen mode Exit fullscreen mode

In above example, we haven't defined the expectation for find method, and it simply returns null. If same mock was created with mockery, it would have thrown exception.

Mockery Mock & Spy: Mockery's Spy and Mock work as per our definition that mock expects expectations and spy stores interactions and allows us to assert against it. Here is an example of Mock,

   /** @test */
   public function github_repository_get_method_with_mock()
   {
       $stub = Mockery::mock(GithubRepository::class);

       $stub->shouldReceive('get')
           ->andReturn([
               ['uuid' => 12345678, 'name' => 'laravel/laravel'],  
               ['uuid' => 12345679, 'name' => 'laravel/framework']
           ]);

       $this->swap(GithubRepository::class, $stub);
       $this->assertTrue(is_array(app()->make(GithubRepository::class)->get()));
   }
Enter fullscreen mode Exit fullscreen mode

As mentioned above, spy doesn't expects expectations, where as mock does, so if we remove $stub->shouldHaveReceived('get') in spy example it simply works, where as removing the Mock's expectation causes error in mock example.

One flexibility about mockery is that expectations can be set even for spy.

   /** @test */
   public function github_repository_get_method_with_spy_wrong()
   {
      $stub = Mockery::spy(GithubRepository::class);

      $stub->shouldReceive('get')
          ->andReturn([
              ['uuid' => 12345678, 'name' => 'Bedram Tamang'],
              ['uuid' => 12345679, 'name' => 'Sajan Lamsal']
          ]);

      $this->swap(GithubRepository::class, $stub);
      $this->assertTrue(is_array(app()->make(GithubRepository::class)->get()));
   }
Enter fullscreen mode Exit fullscreen mode
  1. Dummies: Dummies are fill only parameters in testing, which actually never used. Consider the above create method, which requires instance of RepositoryDTO , but we are doing test doubles all the method. So, we possibly pass a new instance of RepositoryDTO just to fill parameters.
   /** @test */
   public function github_repository_with_dummies()
   {
       $mock = $this->createStub(GithubRepository::class);

       $mock->method('create')
           ->willReturn([]);

       $this->swap(GithubRepository::class, $mock);

       $dummy = new RepositoryDTO();
       dd(app()->make(GithubRepository::class)->create($dummy));
   }
Enter fullscreen mode Exit fullscreen mode
  1. Fakes: are short-cut implementation of real objects, it tries to mimic the actual implementation but in short cut way. Consider another example, in which we define UserRepository interface and a method get() to get list of users from a database as.
   interface UserRepository
   {
       public function get(): Collection|array;
   }
Enter fullscreen mode Exit fullscreen mode

and the actual implementation would be,

   class UserEloquentRepository implements UserRepository
   {
       public function get(): Collection|array
       {
           return User::query()->get();
       }
   }
Enter fullscreen mode Exit fullscreen mode

To illustrate the concept of fake, let's try not to touch database when getting user lists from database. To do so, let's define a UserFakerRepository as

   class UserFakeRepository implements UserRepository
   {
       public SupportCollection $data;

       public function get()
       {
           return $this->data;
       }
   }
Enter fullscreen mode Exit fullscreen mode

and the fake UserFakeRepository is swapped during the test.

   /** @test */
   public function it_fakes_the_user_repositor()
   {
       $userRepository = new UserFakeRepository();

       $userRepository->data = collect();

       $this->swap(UserRepository::class, $userRepository);
       print_r(app()->make(UserRepository::class)->get());
   }
Enter fullscreen mode Exit fullscreen mode

For syntactic sugar, let's define fake method in UserFakeRepository,

   public static function fake(\Closure $closure)
   {
       $class= new static();

       $class->data = $closure();

       app()->instance(UserRepository::class, $class);
   }
Enter fullscreen mode Exit fullscreen mode

and our test would be,

   /** @test */
   public function it_fakes_the_user_repository()
   {
       UserFakeRepository::fake(function () {
           return collect([new User()]);
       });

       print_r(app()->make(UserRepository::class)->get());
   }
Enter fullscreen mode Exit fullscreen mode

Conclusion: In conclusion, Stub can be great to use when the requirement is just to return
some pre defined data from some method. Mock and Spy can be used in similar purpose but also useful to assert expectations. Either of PHPUnit or Mocker can be used for this purpose. And fake can be used to fake whole class, and useful especially when it's difficult to mock or stub.

Top comments (0)