-
Notifications
You must be signed in to change notification settings - Fork 0
What Problem Does it Solve?
Jonathan Taylor edited this page May 13, 2020
·
5 revisions
In CRUD apps, testing code that talks to a database is a frequent problem. The usual solution is to simply mock any calls to the database.
Imagine you have a UserEmailUpdater
class that retrieves a user from the db, performs some logic, and updates the user's email. A test with mocks may look like this:
public function test_it_creates_a_user()
{
$user = mock(User::class);
$user->shouldReceive('setEmail')
->with('[email protected]')
->once();
$userRepository = mock(UserRepository::class);
$userRepository->shouldReceive('findOneBy')
->with(['username' => 'user123')
->once()
->andReturn($user);
$userRepository->shouldReceive('save')
->with($user)
->once();
$userEmailUpdater = new UserEmailUpdater($userRepository);
$userEmailUpdater->updateEmail('user123', '[email protected]');
}
While this works, there are issues with the approach:
- The test is too aware of the implementation details of the
updateEmail
method, resulting in brittle tests. For example, renaming thesave
method and the call to it would cause the test to fail, even though the code itself still works. - On the flip side, the mock means the test is no longer testing the contract of UserRepository, but a mocked version of it. If, for example, the
save
method was renamed but the call to it fromUserEmailUpdater
was not updated, the test would still continue to pass. This means the test gives a false sense of confidence. - It's not immediately clear what the test is doing as there's no explicit assertion.
- A heavy use of mocking leads to tests that have the majority of lines dedicated to setting up mocks, followed by 1 or 2 lines for acting and asserting, making them hard to understand.
The alternative? Let the test hit the database:
public function test_it_creates_a_user()
{
Fabrica::create(User::class, function () {
return [
'username' => 'user123',
'email' => '[email protected]',
];
});
$userEmailUpdater = new UserEmailUpdater(new UserRepository());
$userEmailUpdater->updateEmail('user123', '[email protected]');
self::assertDatabaseContainsEntity(User::class, [
'username' => 'user123',
'email' => '[email protected]',
]);
}
- We no longer care about the implementation details of
UserEmailUpdater
, only the contract of theupdateEmail
method aka its input and output. We only care that the user's email is update in the database, not how the method does it. - We're now testing the real behaviour between
UserEmailUpdater
andUserRepository
. If the test passes, we can be confident it will work in production. - It's got a clear assertion, so it's now more obvious what the test is doing.
- There's a lot less setup code.
Convinced? Well, let's get started.