Search code examples
phpunit-testingphpunitphp-dislim-4

PHPUnit test specific method of class with dependencies


After reading quite some docs I came up with this function to test the service method findAllUsers. I first create a stub of the repository, then tell what the relevant method findAllusers returns (yes they have the same name in repo and service) and then create an instance of the class I want to test UserService.php with the repo stub and empty instances.

public function testFindAllUsers()
{
    $userProvider = new UserProvider();
    $users = $userProvider->getSampleUsers();

    $repoStub = $this->createMock(UserRepository::class);
    $repoStub->method('findAllUsers')->willReturn($users);

    // Here I want to instantiate UserService with my custom repo stub but then I have to add the other dependencies as well
    // They are only empty instances and they don't matter in this test case so it works but is that correct? What should / could I do else?
    $service = new UserService($repoStub, new UserValidation(new Logger(), $repoStub), new Logger());
    $this->assertEquals($users, $service->findAllUsers());
}// Test passes

Constructor of class UserService.php

public function __construct(UserRepository $userRepository, UserValidation $userValidation,LoggerInterface $logger){ ... } 

And UserValidation.php

public function __construct(LoggerInterface $logger, UserRepository $userRepository){ ... }

I don't find it very classy to give empty instances to the class. Is there any cleaner way? I guess what I'm wishing is to create an instance of UserService with only giving the relevant dependency (mocked UserRepository instance) and let the rest be autowired by the di container php-di does that make sense? I've heard about creating a container for testing that returns manually created fake classes with default return values because it's used for integration testing later anyways. I'd love to see some examples and variants.

I am very new to automated testing so any critics about this function is welcomed even if it doesn't directly relate to the question.

Edit

After reading the comment and the answer I think the ideal function that I could imagine is one that registers the UserService class in the container which is instantiated with the wanted custom dependency (in my case the mock of UserRepository) and (for the least amount of overhead) empty mocks of the other dependencies and not only replace the repo with the custom mock and let the autowire feature inject real dependencies. But maybe I'm wrong here, I don't know if the injection of real dependencies is any slower or more overhead.

For that to be possible though the function that registers the new dependency in the container would have to somehow detect the given parameter (containing UserRepository or array if multiple) and the needed missing dependencies (UserValidation and Logger) and mock them and finally create the instance of UserService with cutom UserRepository and the mocked UserValidation and Logger.

This is where my programming and PHP knowledge is limited since I don't know a clean way to programm that. Probably I'm not the first with this and it is very antipattern anyways but yeah would be interesting for me to know your opinions on this thought and why it's a maybe bad idea and how it could be done else.


Solution

  • You could create a mocked repository and set it into the container. The rest will be resolved via autowiring. Make sure that your container is getting reloaded for each test.

    Example

    To set a mocked instance into the container add this method into a trait or base class:

    use PHPUnit\Framework\MockObject\MockObject;
    
    // ...
    
    protected function mock(string $class): MockObject
    {
        $mock = $this->getMockBuilder($class)
            ->disableOriginalConstructor()
            ->getMock();
    
        $this->container->set($class, $mock);
    
        return $mock;
    }
    

    Test Usage

    public function testFindAllUsers()
    {
        // Data provider
        $userProvider = new UserProvider();
        $users = $userProvider->getSampleUsers();
    
        // Mock the required repositories
        $this->mock(UserRepository::class)
          ->method('findAllUsers')
          ->willReturn($users);
    
        // Instantiate UserService
        $service = $this->container->get(UserService::class);
    
        // Compare the result
        $this->assertEquals($users, $service->findAllUsers());
    }