Quick Testing Tips: Self-Contained Tests

Whenever I read a test method I want to understand it without having to jump around in the test class (or worse, in dependencies). If I want to know more, I should be able to “click” on one of the method calls and find out more.

I’ll explain later why I want this, but first I’ll show you how to get to this point.

As an example, here is a test I encountered recently:

public function testGetUsernameById(): void
{
$userRepository = $this->createUserRepository();

$username = $userRepository->getUsernameById(1);

self::assertSame(‘alice’, $username);
}

The way I read this:

/*
* Ah, we’re testing the UserRepository, so we instantiate it.
* The factory method probably injects a connection to the test
* database or something:
*/
$userRepository = $this->createUserRepository();

/*
* Now we fetch a username by its ID. The ID is 1. That’s the
* first time I see it in this test. This probably means that
* there is no user with this ID and the method will throw
* an exception or return a default name or something:
*/
$username = $userRepository->getUsernameById(1);

/*
* Wait, the username is supposed to be “alice”?
* Where did that come from?
*/
self::assertSame(‘alice’, $username);

So while trying to understand this test that last line surprised me. Where does Alice come from?

As it turns out there is a setupTables() function which is called during the setup phase. This method populates the database with some user data that is used in various ways by one of the test methods in the class.

private function setupTables(): void
{
$this->connection->table(‘users’)
->insert(
[
[‘user_id’ => 1, ‘username’ => ‘alice’, ‘password’ => ‘alicepassword’],
[‘user_id’ => 2, ‘username’ => ‘bob’, ‘password’ => ‘bobpassword’],
[‘user_id’ => 3, ‘username’ => ‘john’, ‘password’ => ‘johnpassword’],
[‘user_id’ => 4, ‘username’ => ‘peter’, ‘password’ => ‘peterpassword’],
]
);
// …
}

There are some problems with this approach:

It’s not clear which tests rely on which database records (a common issue with shared database fixtures). So it’s hard to change or remove tests, or the test data, when needed. As an example, if we remove one test, maybe some test data could also be removed but we don’t really know. If we change some test data, one of the tests may break.
It’s not clear which of the values is actually relevant. For example, we’re interested in user 1, ‘alice’, but is the password relevant? Most likely not.

The first thing we need to do is ensure that each test only creates the database records that it really needs, e.g.

public function testGetUsernameById(): void
{
$this->connection->table(‘users’)
->insert(
[
‘user_id’ => 1,
‘username’ => ‘alice’,
‘password’ => ‘alicepassword’
]
);

$userRepository = $this->createUserRepository();

$username = $userRepository->getUsernameById(1);

self::assertSame(‘alice’, $username);
}

At this point the test is already much easier to understand on its own. You can clearly see where the number 1 and the string ‘alice’ come from. There’s only that ‘alicepassword’ string that is irrelevant for this test. Leaving it out gives us an SQL constraint error. But we can still get rid of it here by extracting a method for creating a user record, moving the insert() out of sight:

public function testGetUsernameById(): void
{
$this->createUser(1, ‘alice’);

$userRepository = $this->createUserRepository();

$username = $userRepository->getUsernameById(1);

self::assertSame(‘alice’, $username);
}

private function createUser(int $id, string $username): void
{
$this->connection->table(‘users’)
->insert(
[
‘user_id’ => $id,
‘username’ => $username,
‘password’ => ‘a-password’
]
);
}

Going back to the beginning of this post:

When I read a test method I want to understand it without having to jump around in the test class (or worse, in dependencies).
If I want to know more, I should be able to “click” on one of the method calls and find out more.

With just a few simple refactoring steps we’ve been able to achieve these things. As a consequence we achieve the greater goal, the reason why I stick to these rules: each test method is now self-contained, meaning we can delete or change any of them without influencing the other test methods.

At the point where a test is self-contained like this, I try to go the extra mile by rephrasing it using the Given/When/Then syntax:

Given the user with ID 1 has username “alice”
When getting the username of the user with ID 1
Then the username is “alice”

In my opinion this doesn’t add much insight and only shows that the repository can do a SELECT query for something that was just INSERTed. The big question here is: why do we even have to find out what the username is? Once we know we should codify the answer to this question in a test. So instead of testing that single repository method, I’d rather see it being used in its bigger context and read the test for that. E.g.

Given user 1 has username “alice”
When we send a mail to this user
Then the footer of the mail shows “To find out more, log in with your username: alice”

This is actually much better, since this test takes a much safer distance to the subject under test; it leaves the design design to use a repository method for finding the username an implementation detail.

This post has been inspired by some development coaching work I’m doing for PinkWeb at the time of writing. Check out their vacancies if you’d like to join the team as well!

Leave a Reply

Your email address will not be published. Required fields are marked *