Codeskill

Learn to code, step by step

Testing – PHPUnit habits that pay off

PHPUnit habits that save time – not a full testing textbook. You already know why tests matter; here is how to write ones you will not delete in six months.

Setup

composer require --dev phpunit/phpunit
./vendor/bin/phpunit

Put tests in tests/, mirror your src/ namespace. One test class per production class is a fine default.

Test behaviour, not implementation

Good tests read like specifications: “given X, when Y, expect Z”. Bad tests assert that you called a private method three times – those break on every refactor.

public function test_empty_title_is_rejected(): void
{
    $note = new Note(1, 5, 'Draft', 'Body');

    $this->expectException(InvalidArgumentException::class);
    $note->rename('   ');
}

Arrange, act, assert

Three clear blocks in every test. Blank line between them. Your future self will thank you.

public function test_admin_can_delete_any_note(): void
{
    // Arrange
    $auth = new Authorizer();
    $note = new Note(1, 99, 'Title', 'Body');

    // Act
    $allowed = $auth->canDelete('admin', $note, 5);

    // Assert
    $this->assertTrue($allowed);
}

Use fakes for boundaries

Do not hit MySQL in unit tests. Pass an in-memory repository or a stub:

final class InMemoryNoteRepository implements NoteRepository
{
    /** @var array<int, Note> */
    private array $notes = [];

    public function save(Note $note): void
    {
        $this->notes[$note->id] = $note;
    }

    public function findById(int $id): ?Note
    {
        return $this->notes[$id] ?? null;
    }
}

Integration tests (real database, one or two) belong in a separate suite you run less often – CI nightly, or before release.

Data providers for tables of cases

/**
 * @dataProvider roleProvider
 */
public function test_permissions(string $role, string $perm, bool $expected): void
{
    $auth = new Authorizer();
    $this->assertSame($expected, $auth->can($role, $perm));
}

public static function roleProvider(): array
{
    return [
        ['admin', 'users.manage', true],
        ['user', 'users.manage', false],
        ['editor', 'notes.delete', true],
    ];
}

Habits that pay off

  • Run tests before every commit – even if CI also runs them
  • When you fix a bug, write the test that would have caught it first
  • Keep tests fast; slow suites get skipped
  • Name tests after the scenario, not the method: test_guest_cannot_access_dashboard
  • Avoid testing the framework; test your rules

PHPUnit is not magic. It is a safety net for the code you actually care about – domain rules, authorisation, validation. Start there; expand when it hurts not to.

PreviousCaching strategies – opcode, data, HTTP