Skip to content

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:

  1. The test is too aware of the implementation details of the updateEmail method, resulting in brittle tests. For example, renaming the save method and the call to it would cause the test to fail, even though the code itself still works.
  2. 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 from UserEmailUpdater was not updated, the test would still continue to pass. This means the test gives a false sense of confidence.
  3. It's not immediately clear what the test is doing as there's no explicit assertion.
  4. 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]',
    ]);
}
  1. We no longer care about the implementation details of UserEmailUpdater, only the contract of the updateEmail 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.
  2. We're now testing the real behaviour between UserEmailUpdater and UserRepository. If the test passes, we can be confident it will work in production.
  3. It's got a clear assertion, so it's now more obvious what the test is doing.
  4. There's a lot less setup code.

Convinced? Well, let's get started.

Clone this wiki locally