Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Elegant way to testing the Services/Service Containers in Symfony3 with PHPUnit

I'm learning recently about the Symfony 3 framework and Dependency Injection.

I would like you to help me solve my doubts about the method of testing Services in Symfony 3 using PHPUnit. I have some concerns how to do it right way.

Lets make an example of Service class:

// src/AppBundle/Services/MathService.php
namespace AppBundle\Services;

class MathService
{
    public function subtract($a, $b)
    {
        return $a - $b;
    }
}

I see that usually the UnitTest classes in Symfony tests the Controllers.

But what can I test independent classes like Services (which have business logic included for example) instead of Controllers ?

I know there are at least 2 ways to do it:


1. Create a Test Class which extends the PHPUnit_Framework_TestCase and create the object of Service inside some methods or constructor in this Test Class (exactly like in Symfony docs about testing)

// tests/AppBundle/Services/MathTest.php
namespace Tests\AppBundle\Services;

use AppBundle\Services\MathService;

class MathTest extends \PHPUnit_Framework_TestCase
{
    protected $math;

    public function __construct() {
        $this->math = new MathService();
    }

    public function testSubtract()
    {
        $result = $this->math->subtract(5, 3);
        $this->assertEquals(2, $result);
    }
}

2. Make our Service class as a Service Container using Dependency injection. Then create a Test Class which extends the KernelTestCase to get access to the Kernel. It will give us ability to inject our Service using Container from Kernel (based on Symfony docs about testing Doctrine).

Configuration of Service Container:

# app/config/services.yml
services:
    app.math:
        class: AppBundle\Services\MathService

Now our Test Class will looks like:

// tests/AppBundle/Services/MathTest.php
namespace Tests\AppBundle\Services;

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

class MathTest extends KernelTestCase
{
    private $math;

    protected function setUp()
    {
        self::bootKernel();

        $this->math = static::$kernel
            ->getContainer()
            ->get('app.math');
    }

    public function testSubtract()
    {
        $result = $this->math->subtract(5, 3);
        $this->assertEquals(2, $result);
    }
}

There are benefits when we choose this way.

Firstly we have access to our Service Container in controllers and tests through Dependency Injection.

Secondly - if in the future we want to change the location of Service class or change the name of class - compared with 1. case - we can avoid changes in many files, because we will change path/name at least in services.yml file.


My questions:

Are there another ways to test Service class in Symfony 3 ? Which way is better and should be used?

like image 714
Mateusz Palichleb Avatar asked Nov 18 '16 22:11

Mateusz Palichleb


1 Answers

UPDATED 2018 with tricky Symfony 3.4/4.0 solution.

This approach with all its pros/cons is described in this post with code examples.


The best solution to access private services is to add a Compiler Pass that makes all services public for tests.

1. Update Kernel

 use Symfony\Component\HttpKernel\Kernel;
+use Symplify\PackageBuilder\DependencyInjection\CompilerPass\PublicForTestsCompilerPass;

 final class AppKernel extends Kernel
 {
     protected function build(ContainerBuilder $containerBuilder): void
     {
         $containerBuilder->addCompilerPass('...');
+        $containerBuilder->addCompilerPass(new PublicForTestsCompilerPass());
     }
 }

2. Require or create own Compiler Pass

Where PublicForTestsCompilerPass looks like:

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

final class PublicForTestsCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $containerBuilder): void
    {
        if (! $this->isPHPUnit()) {
            return;
        }

        foreach ($containerBuilder->getDefinitions() as $definition) {
            $definition->setPublic(true);
        }

        foreach ($containerBuilder->getAliases() as $definition) {
            $definition->setPublic(true);
        }
    }

    private function isPHPUnit(): bool
    {
        // defined by PHPUnit
        return defined('PHPUNIT_COMPOSER_INSTALL') || defined('__PHPUNIT_PHAR__');
    }
}

To use this class, just add the package by:

composer require symplify/package-builder

But of course, the better way is to use own class, that meets your needs (you migt Behat for tests etc.).

Then all your tests will keep working as expected!

like image 140
Tomas Votruba Avatar answered Oct 30 '22 11:10

Tomas Votruba