DEV Community

Cover image for Argument capture in PHP tests
Fabio Hiroki
Fabio Hiroki

Posted on • Edited on

Argument capture in PHP tests

You're already feeling confident with your test skills and all your teammates envy you talking about mocks and stubs. But now you're faced with a new challenge, you only need to verify some properties of an argument, otherwise your tests will fail miserably.

Gif showing someone horrorified

Sunny day starts

Your job was to implement a simple user registration feature. As a good programmer you planned this to use the Repository pattern so you could separate the persistence layer and mock it in unit tests if necessary.

Gif of someone thinking this is going to be easy

User class

Couldn't be simpler, right?

final class User
{
    public function __construct(
        private string $id,
        private string $name)
    {
    }

    public function getId(): string
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }
}
Enter fullscreen mode Exit fullscreen mode

UserRepository

Not doing any persistence, but let's pretend it is.

final class UserRepository
{
    public function save(User $user): void
    {
        echo sprintf('Persisted user with id %s and name %s', $user->getId(), $user->getName());
    }
}
Enter fullscreen mode Exit fullscreen mode

UserService

Now we have some plot twist! Some crazy requirements arrived from above and it says the id field should be randomic. Well, there's a lot of ways to achieve this so you just go on:

final class UserService
{
    public function __construct(private UserRepository $repository) { }

    public function createNewUser(string $name): void
    {
        $user = new User(uniqid(), $name);

        $this->repository->save($user);
    }
}
Enter fullscreen mode Exit fullscreen mode

Cool, everything is working.

Wait, let's not forget the tests

Gif of someone rembering to test

We want to verify when the UserService creates an User, if the UserRepository is being called with correctly. We don't care now about the persistence, so we can just mock the repository and everything should work fine.

public function testFailing(): void
{
    $repository = $this->createMock(UserRepository::class);

    $userService = new UserService($repository);

    $repository
        ->expects($this->once())
        ->method('save')
        ->with(new User('not so random id', 'user'));

    $userService->createNewUser('user');
}
Enter fullscreen mode Exit fullscreen mode

Until it doesn't. That test fails because User with not so random id doesn't match the argument called by UserRepository which has a different id each time a new user is created.

Argument capture to the rescue!

In Java we have this same feature enabled by Mockito:

Argument Capture allows us to create assertions on certain values of the arguments, instead of testing the equality for the whole object.

In our example we don't need to test the id value, since it's randomic and it's being generated by PHP itself using the uniqid function.

In the end this is how the test using argument capture looks like:

public function testRepositoryShouldCreateUserWithCorrectName(): void
{
    $repository = $this->createMock(UserRepository::class);

    $userService = new UserService($repository);

    $repository
        ->expects($this->once())
        ->method('save')
        ->will($this->returnCallback(function($user) {
            self::assertEquals('user', $user->getName());
        }));

    $userService->createNewUser('user');
}
Enter fullscreen mode Exit fullscreen mode

Thanks for reading this far, hope you enjoyed and good luck with your tests!

Good luck!

Top comments (0)