Replace Symfony service in tests for php 7.2
Asked Answered
A

1

12

I'm trying to upgrade my application working on Symfony 3.3 and php 7.1 to php 7.2, but I encountered tons of DEPRECATED messages when I run phpunit. Most annoying is:

The "user.user_service" service is already initialized, replacing it is deprecated since Symfony 3.3 and will fail in 4.0: 7x

This is because I have this lines at setUp method:

$this->userService = $this->getMockBuilder(UserService::class)
    ->setMethods(['update'])
    ->getMock();
$container->set('user.user_service', $this->userService);

7x is because I have 7 test cases at that class, and setUp is fired for each of them. How could I handle this issue? I can't remove this mock because it's important.

I can't understand why Symfony point exactly to this testcase, because I have lots of services replaced this way across all my tests. I don't replace this service anywhere before this setUp method, so it's strange.

Anderson answered 25/6, 2018 at 15:11 Comment(4)
Can you show how you are initializing your container?Breve
@Jason Roman, Sure, I use $container = static::$kernel->getContainer();Anderson
Can you update the question in one complete test case, please?Occiput
You could try this: $this->container->getDefinition('user.user_service')->setSynthetic(true); before doing your $container->set call.Breve
I
1

So, after build the container, container's immutable and you can't add other services/parameters to it (replace too, because this contain two operation - delete and set).

But, in more cases we should have any opportunities for replace logic inside a our service.

For this, I use custom services only for tests. This service override default service (class TestUserService extends UserService) or use interfaces (better). After create class I declare this service with same name in app/config/test.yml (when@test) or via another opportunities.

namespace Acme;

interface UserServiceInterface
{
    public function create(object $some): User;
}

/* Original service */
readonly class UserService implements UserServiceInterface
{
    public function create(object $some): User
    {
        // Some actions here
    }
}

/* Service only for test */
class TestUserService implements UserServiceInterface
{ 
    private ?\Closure $callback;
    
    public function shouldExecute(\Closure $callback): void
    {
        $this->callback = $callback;
    }

    public function create(object $some): User
    {
        if ($this->callback) {
            return ($this->callback)($some);
        }
        
        throw new \RuntimeException(\sprintf(
            'The service %s wrong configured. Please call "shouldExecute" method previously.',
            __CLASS__
        ));
    }
}

And declare it in you config (YAML as an example):

services:
    user_service:
        class: Acme\UserService

when@test:
    services:
        user_service:
            class: Acme\TestUserService

After this, I can easy use any logic inside UserService::create only for test environment.

class SomeTest extends TestCase
{
    private TestUserService $userService;
    
    protected function setUp(): void
    {
        $this->userService = self::getContainer()->get('user_service');
    }
    
    public function shouldSuccess(): void
    {
        $this->userService->shouldExecute(static function (object $some) {
            self::assertEquals('foo bar', $some);
            
            return new User();
        });
        
        // logic for testing
    }
}

In few cases, developer want to have opportunities to change logic only in specific cases. For this we can use decorator pattern and call origin UserService inside TestUserService if callback not configured.

Illiteracy answered 24/2 at 9:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.