Test Doubles in PHP with Mockery and PHPUnit
Published: 2023-06-29 01:52:39

**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 spies[1]. The name comes from the notion of a Stunt Double in movies[1]. **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,

```php
interface GithubRepository
{
    public function find(string $uuid);
    
    public function get();
    
    public function create(RepositoryDTO $userDto);
}
```



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:

   ```php
   /** @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()));   
    }
   ```

   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.

2. **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.

   ```php
   /** @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)));
   }
   ```

   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.

   ```php
   $mock = $this->createMock(GithubRepository::class);
   $this->swap(GithubRepository::class, $mock);
     
   print_r(app()->make(GithubRepository::class)->find(1));
   ```

   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,

   ```php
   /** @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()));
   }
   ```

   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.

   ```php
   /** @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()));
   }
   ```

3. **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.

   ```php
   /** @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));
   }
   ```

4. **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.

   ```php
   interface UserRepository
   {
       public function get(): Collection|array;
   }
   ```

   and the actual implementation would be,

   ```php
   class UserEloquentRepository implements UserRepository
   {
       public function get(): Collection|array
       {
           return User::query()->get();
       }
   }
   ```

   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

   ```php
   class UserFakeRepository implements UserRepository
   {
       public SupportCollection $data;
       
       public function get()
       {
           return $this->data;
       }
   }
   ```

   and the fake `UserFakeRepository` is swapped during the test.

   ```php
   /** @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());
   }
   ```

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

   ```php
   public static function fake(\Closure $closure)
   {
       $class= new static();
       
       $class->data = $closure();
       
       app()->instance(UserRepository::class, $class);
   }
   ```

   and our test would be,

   ```php
   /** @test */
   public function it_fakes_the_user_repository()
   {
       UserFakeRepository::fake(function () {
           return collect([new User()]);
       });
       
       print_r(app()->make(UserRepository::class)->get());
   }
   ```

   **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. 


[1]: https://martinfowler.com/bliki/TestDouble.html