Test doubles w PHPUNIT

Test doubles to technika w testowaniu jednostkowym, która polega na tworzeniu obiektów zastępczych dla rzeczywistych obiektów w systemie. Są one używane do izolacji jednostki testowanej i kontrolowania jej zachowania w testach.

W PHPUnit, możemy używać różnych typów test doubles, takich jak:

Dummy: Są to obiekty, które są przekazywane do testowanej jednostki, ale nigdy nie są używane.
Fake: Obiekty, które mają działającą implementację, ale są uproszczone do celów testowych.
Stubs: Obiekty, które zwracają wcześniej zdefiniowane odpowiedzi na wywołania metod.
Mocks:
Obiekty, które oczekują, że określone metody zostaną wywołane z określonymi parametrami.
Spy: Obiekty, które rejestrują informacje o interakcjach i pozwalają na późniejszą weryfikację.

Atrapa (Dummy)

class OrderProcessorTest extends \PHPUnit\Framework\TestCase
{
    public function testProcessOrder()
    {
        $dummyLogger = $this->createMock(Logger::class);
        $orderProcessor = new OrderProcessor($dummyLogger);

        $order = new Order();
        $orderProcessor->process($order);

        $this->assertTrue($order->isProcessed());
    }
}

Własna implementacja

final class DummyEntityRepository implements RepositoryInterface
{
    public function save(Entity $entity): void
    {
    }
}

Zaślepka (Stub)

Obiekt danej klasy gdzie wszystkie metody, pola zwracają puste wartości NULL. Jest to nic innego jak możliwie najprostsza implementacja danego interfejsu, która w zasadzie na nic nie reaguje ponieważ jej zachowanie jest z góry zdefiniowane. Stubem byłaby implementacja czujnika prędkości zwracająca konkretną wartość.

class UserServiceTest extends \PHPUnit\Framework\TestCase
{
    public function testGetUser()
    {
        $userStub = $this->createMock(UserRepository::class);
        $userStub->method('find')->willReturn(new User('john@example.com'));

        $userService = new UserService($userStub);
        $user = $userService->getUser('john@example.com');

        $this->assertEquals('john@example.com', $user->getEmail());
    }
}

Własna implementacja

final class StubEntityRepository implements RepositoryInterface
{
    public function findOneByEmail(string $email): ?Entity
    {
        return new Entity($email);
    }
}

Imicjacja (Mock)

Mock to tak jakby zaprogramowana imitacja, której nie tylko przekazujemy nasze oczekiwania ale również mówimy jak się ma zachować.

class UserServiceTest extends \PHPUnit\Framework\TestCase
{
    public function testCreateUser()
    {
        $userMock = $this->createMock(UserRepository::class);
        $userMock->expects($this->once())
                 ->method('save')
                 ->with($this->isInstanceOf(User::class));

        $userService = new UserService($userMock);
        $userService->createUser('john@example.com');
    }
}

Szpieg (Spy)

Imitacja której celem jest nie tyle zrobienie czegoś konkretnego co sprawdzenie czy coś zostało zrobione ale bez definiowania z góry naszych oczekiwań. Spy używamy w celu zweryfikowania jakiegoś zachowania.

class UserServiceTest extends \PHPUnit\Framework\TestCase
{
    public function testCreateUserWithSpy()
    {
        $userSpy = new UserRepositorySpy();
        $userService = new UserService($userSpy);

        $userService->createUser('john@example.com');

        $this->assertTrue($userSpy->wasSaveCalled());
        $this->assertEquals('john@example.com', $userSpy->getLastSavedUser()->getEmail());
    }
}

class UserRepositorySpy extends UserRepository
{
    private $saveCalled = false;
    private $lastSavedUser;

    public function save(User $user)
    {
        $this->saveCalled = true;
        $this->lastSavedUser = $user;
    }

    public function wasSaveCalled(): bool
    {
        return $this->saveCalled;
    }

    public function getLastSavedUser(): ?User
    {
        return $this->lastSavedUser;
    }
}

Podróbka (Fake)

Tworząc fake, tworzymy tak naprawdę imitację bardzo zbliżoną do oryginału, w dalszym stopniu jednak uproszczoną, najczęściej po to aby zasymulować konkretne zachowanie zależne od parametrów wejściowych.

class FakeDatabaseConnection
{
    private $data = [];

    public function insert($table, $record)
    {
        $this->data[$table][] = $record;
    }

    public function getAll($table)
    {
        return $this->data[$table] ?? [];
    }
}

class UserServiceTest extends \PHPUnit\Framework\TestCase
{
    public function testCreateUser()
    {
        $fakeDb = new FakeDatabaseConnection();
        $userService = new UserService($fakeDb);

        $userService->createUser('john@example.com');

        $users = $fakeDb->getAll('users');
        $this->assertCount(1, $users);
    }
}
Moje początki programowania sięgają 2010r. W trakcie wielu projektów zdobywałem doświadczenie, rozwijając nie tylko umiejętności techniczne, ale także kompetencje miękkie. Programuje głównie w PHP i Python.