How is it possible to test services with Laravel using PhpUnit?

Viewed 7436

I would like to test some of my services, but I can not find any example on Laravel's website: https://laravel.com/docs/5.1/testing

They show how to test simple classes, entities, controllers, but I have no idea how to test services. How is it possible to instantiate a service with complex dependencies?


Example service:

<?php

namespace App\Services;

// Dependencies
use App\Services\FooService;
use App\Services\BarService;

class DemoService {

    private $foo_srv;
    private $bar_srv;

    function __construct(
        FooService $foo_srv,
        BarService $bar_srv
    ) {
        $this->foo_srv = $foo_srv;
        $this->bar_srv = $bar_srv;
    }

    // I would like to test these two functions

    public function demoFunctionOne() {
        // ...
    }

    public function demoFunctionTwo() {
        // ...
    }

}
4 Answers

The quickest thought that might come to the mind is to create a copy of those Service classes, but that could grow so big. This is why there's MockObject in PhpUnit.

To achieve this mock and use it as a replacement to the service class you may need to resolve it in Laravel service container.

Here is how it will look:

class DemoServiceTest extends TestCase
{
    // Dependencies
    use App\Services\FooService;
    use App\Services\BarService;
    use App\Services\DemoService;

    public function testDemoFunctionOne()
    {
        $foo_srv = $this->getMockBuilder(FooService::class)
            //->setMethods(['...']) // array of methods to set initially or they return null
            ->disableOriginalConstructor() //disable __construct
            ->getMock();
         /**
          * Set methods and value to return
          **/
        // $foo_srv->expects($this->any())
        //    ->method('myMethod') //method needed by DemoService?
        //    ->will($this->returnValue('some value')); // return value expected

        $bar_srv = $this->getMockBuilder(BarService::class)
//            ->setMethods(['...']) // array of methods to set initially or they return null
            ->disableOriginalConstructor()
            ->getMock();

        /**
         * Set methods and value to return
         **/
        // $bar_srv->expects($this->any())
        //    ->method('myMethod') //method needed by DemoService?
        //    ->will($this->returnValue('some value')); // return value expected

        $demo_service = new DemoService($foo_srv, $bar_srv);
        $result = $demo_service->demoFunctionOne(); //run demo function

        $this->assertNotEmpty($result); //an assertion
    }
}

We are creating new mock for both FooService and BarService classes, and then passing it when instantiating DemoService.

As you can see without uncommenting the commented chunk of code, then we set a return value, otherwise when setMethods is not used, all methods are default to return null.

Let's say you want to resolve these classes in Laravel Service Container for example then you can after creating the mocks, call:

$this->app->instance(FooService::class, $foo_srv);
$this->app->instance(BarService::class, $bar_srv);

Laravel resolves both classes, feeding the mock classes to any caller of the classes So that you just call your class this way:

$demoService = $this->app->make(DemoService::class);

There are lots of things to watch out for when mocking classes for tests, see sources: https://matthiasnoback.nl/2014/07/test-doubles/, https://phpunit.de/manual/6.5/en/test-doubles.html

I have an example using the service you gave below. The basic idea is that for dependencies you just assume that they'll work as expected and create mocks for them.

Mocks are special classes that pretend to be a different class, but don't really do anything unless you tell them to. For example, say we have a class called UserCreationService that takes a few arguments required to create a new user. This class depends on some things like a Mailer class to send a registration mail, and a UserRepository class to save the user to the database. It also does a lot of validation of the user, and we'd like to check lots and lots of edge cases for all the different possible arguments to create a user.

In this example do we really want to check that the user was saved to the database? Do we want to check for sure that the mail was really sent? We could do that, but it would take a very long time to run all our test cases. Instead we just assume that the classes our UserCreationService depends on will just do their job and we create mock classes for the dependencies. We would create mocks for the Mailer and UserRepository and just tell the test that we expect some methods to be called (like sendRegistrationMail for the Mailer) and concentrate on the logic contained in our class.

// This is the class we want to test
class UserCreationService {

    // The dependencies
    private $userRepository;
    private $mailer;

    public function __construct($userRepository, $mailer)
    {
        $this->userRepository = $userRepository;
        $this->mailer = $mailer;
    }

    public function create($name, $email, $location, $age)
    {
        $user = new User();
        // do some complex validation here. This is what we want to test
        $this->validateName($name);
        $this->validateEmail($email);
        $this->validateLocation($location);
        $this->validateAge($age);

        // then call our external services
        $this->userRepository->save($user);
        $this->mailer->sendRegistrationMail($user);
    }
}

// This is a sample test for the above class using mocking
class UserCreationServiceTest extends TestCase
{
    public function testValidUserWillBeSaved() {
        // The framework allows using argument tokens.
        // This just means an instance of *any* user class is expected
        $anyUserToken = Argument::type(User::class);

        // The prophesize method creates our mock for us
        // We then define its behavior
        $mailer = $this->prophesize(Mailer::class);
        // Let our test know we expect this method to be called
        // And that we expect an instance of a user class to be passed to it
        $mailer->sendRegistrationMail($anyUserToken)->shouldBeCalled();

        $userRepo = $this->prophesize(UserRepository::class);
        $userRepo->save($anyUserToken)->shouldBeCalled();

        // Create our service with the mocked dependencies
        $service = new UserCreationService(
            $userRepo->reveal(), $mailer->reveal()
        );
        // Try calling our method as part of the test
        $result = $service->create('Tom', 'tom@test.com', 'Ireland', 24);

        // Do some check to see that the result we got is what we expected
        $this->assertEquals('Tom', $result->getName());
    }
}

This is something similar, but specific to the example you gave above:

use PHPUnit\Framework\TestCase;

class DemoServiceTest extends TestCase
{
    public function testDemoFunctionOne()
    {
        // These are the variables that will be passed around the service
        $sampleId = 1;
        $myModel = new MyModelClass();

        // Set up a mock FooService instance
        $fooMock = $this->prophesize(FooService::class);
        // Tell the test that we expect "findFoo" to be called and what to return
        $fooMock->findFoo($sampleId)->willReturn($myModel);

        // Set up a mock BarService instance
        $barMock = $this->prophesize(BarService::class);
        // Tell the test we expect "save" to be called and what argument to expect
        $barMock->save($myModel)->shouldBeCalled();

        // Create an instance of the service you want to test with the mocks
        $demoService = new DemoService($fooMock->reveal(), $barMock->reveal());
        // Call your method, get a result
        $result = $demoService->demoFunctionOne($sampleId);

        // Check that the result is what you want
        $this->assertEquals($myModel, $result);
    }
}

I'd have a look at Laravel specific stuff here and prophecy here

i am not sure about the context you have there, but I will try to have an answer for you based on an example. Imagine that you want to test a payment gateway from a payment provider. My approach is to make 2 payment gateways extend something like this interface:

<?php
namespace App\Payment;

interface PaymentGateway
{
    public function charge($amount, $token);

    public function getTestToken();

    ....
}

then i will create the real payment gateway that is used in the controllers or where ever you need it and for the tests the 'fake' payment and this is an exact copy of the real one but with dummy data. This you can use on the tests because it is faster and it is a 1to1 copy of the real. I think if the service it self works or not via the internet it is outside of the testing scope, at least for now. So you will end up with something like this:

<?php
namespace App\Payment;

class FakePaymentGateway implements PaymentGateway
{

    private $tokens;

    const TEST_CARD_NUMBER = '1234123412341234';

    public function __construct()
    {
        $this->tokens = collect();
    }

    public function getTestToken()
    {
        return 'fake-tok_'.str_random(15);
    }
....
}

and the real one:

<?php

namespace App\Payment;

class PaypalPaymentGateway implements PaymentGateway
{
    ...

    public function __construct(PayPal $PaypalClient)
    {
        ...
    }

    public function charge($amount, $token)
    {
        ...
    }
    ....
}

so i think in your case, when you have complex dependencies, you have to fake all of that, depending on the case, into the fake service.

the tests will look like this for the fake service:

<?php namespace Tests\Unit\Payment;

use App\Payment\FakePaymentGateway;
use Tests\TestCase;

class FakePaymentGatewayTest extends TestCase
{
    use PaymentGatewayContractTests;

    protected function getPaymentGateway()
    {
        return new FakePaymentGateway;
    }
...
}

for the real one like this:

<?php namespace Tests\Unit\Payment;

use App\Payment\PaypalPaymentGateway;
use Tests\TestCase;

/**
 * @group integration
 *
 * ./vendor/phpunit/phpunit/phpunit --exclude-group integration
*/
class PaypalPaymentGatewayTest extends TestCase
{
    use PaymentGatewayContractTests;

    protected function getPaymentGateway()
    {
       return new PaypalPaymentGateway(....);
    }
    ...
}

for the real one you should ignore it in the phpunit when running to make the tests faster and not depending of the internet connection and so on. It is also nice to have in the tests suite, when changes from the service occur.

You will end up having a better understanding of the dependencies and maybe you will also do some refactoring. Anyway i guess it is a lot to work but when the real implemanation will change you can see that also from the tests and change it faster.

I hope my answer will help you in your service tests :).

Related