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 th
Truncated by Planet PHP, read more at the original (another 1692 bytes)