Argument capture in PHP tests

Fabio Hiroki
3 min readOct 18, 2021

--

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.

Oh no, tests are failing!

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.

Wait, this is too easy to be true

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;
}
}

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());
}
}

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);
}
}

Cool, everything is working.

Wait, let’s not forget the tests

We should, definitely

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');
}

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 uniqidfunction.

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');
}

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

--

--

No responses yet