Test doubles

From Freephile Wiki

A wp:Test double is any object that stands in for a real dependency in automated testing. Usually in PHPUnit, we make test doubles for other classes, but you can also double PHP native functions or closures[1][2].

There are 5 types of Test doubles

Dummy
"A dummy argument" - used as a placeholder when an argument needs to be passed in.
Stub
Provides fake or 'canned' data to the System Under Test (SUT).
Spy
Records information about how it is used, and can provide that information back to the test.
Mock
Defines an expectation on how it will be used, and with what parameters. Will cause a test to fail automatically if the expectation isn’t met.
Fake
"Looks real, but it's not." An actual (simplified) implementation of the contract, unsuitable for production.


Dummy[edit]

"A dummy argument" - used as a placeholder when an argument needs to be passed in.

In the example code below, we have a User class that we want to write a test for. The User object needs a Logger object passed as a parameter to its constructor. So, in the UserTest class, we create a dummy to pass to the system under test. The test double "Logger" doesn't have to do anything at all - it just exists to be passed as an argument during the test.

class User {
    public function __construct(private Logger $logger) {}
}

class Logger {}

class UserTest extends \PHPUnit\Framework\TestCase {
    public function testUser() {
        $dummyLogger = $this->createMock(Logger::class);
        $user = new User($dummyLogger);
        $this->assertInstanceOf(User::class, $user);
    }
}

Stub[edit]

Provides fake or 'canned' data to the System Under Test (SUT).

A stub is an object that provides predefined responses to method calls. It is used to control the behavior of the System Under Test, "steering it" in a particular direction.

In the example below, the Logger->log() method does some stuff with a message. For our test scenario, we use a Stub of the Logger class where we define the log() method to always return 'true'. Why do that? Since the log method always returns true, we can focus our test on another aspect of the System Under Test that relies on or is dependent on, the log method returning true. (Sorry that no actual example is given in the unimaginative code below. Supose you had a system that detects outdoor temperature and tells you when it is safe to skate on the ice. You could force (stub) the temp detection to read "freezing" to assert that the signal "It is safe to skate" is given by the SUT.)

<?php
class User {
    public function __construct(private Logger $logger) {}

    public function logMessage($message) {
        return $this->logger->log($message);
    }
}

class Logger {
    public function log($message) {
        // Actual logging logic
    }
}

class UserTest extends \PHPUnit\Framework\TestCase {
    public function testLogMessage() {
        $stubLogger = $this->createStub(Logger::class);
        $stubLogger->method('log')->willReturn(true);

        $user = new User($stubLogger);
        $this->assertTrue($user->logMessage('Test message'));
    }
}

PHPUnit Mock Builder[edit]

In PHPUnit, you can simply call $this->createStub(SomeClass::class), but you can also be more refined using the Mock Builder fluent interface.


Spy[edit]

Records information about how it is used, and can provide that information back to the test.

A spy is a test double that records information about the interactions with it, such as method calls and parameters. It is used to verify that certain interactions occurred.

Notice in the example below, the code is ordered differently from the preferred 'AAA' Test Pattern of Arrange, Act, Assert[3]. Below, we Arrange, Assert, then Act.

class User {
    public function __construct(private Logger $logger) {}

    public function logMessage($message) {
        $this->logger->log($message);
    }
}

class Logger {
    public function log($message) {
        // Actual logging logic
    }
}

class UserTest extends \PHPUnit\Framework\TestCase {
    public function testLogMessage() {
        $spyLogger = $this->createMock(Logger::class);
        $spyLogger->expects($this->once())
                  ->method('log')
                  ->with($this->equalTo('Test message'));

        $user = new User($spyLogger);
        $user->logMessage('Test message');
    }
}


Mock[edit]

class User {
    public function __construct(private Logger $logger) {}

    public function logMessage($message) {
        $this->logger->log($message);
    }
}

class Logger {
    public function log($message) {
        // Actual logging logic
    }
}

class UserTest extends \PHPUnit\Framework\TestCase {
    public function testLogMessage() {
        $mockLogger = $this->createMock(Logger::class);
        $mockLogger->expects($this->once())
                   ->method('log')
                   ->with($this->equalTo('Test message'));

        $user = new User($mockLogger);
        $user->logMessage('Test message');
    }
}

Good reads: Mocks Aren't Stubs
- Martin Fowler
02 January 2007

The term 'Mock Objects' has become a popular one to describe special case objects that mimic real objects for testing. Most language environments now have frameworks that make it easy to create mock objects. What's often not realized, however, is that mock objects are but one form of special case test object, one that enables a different style of testing. In this article I'll explain how mock objects work, how they encourage testing based on behavior verification, and how the community around them uses them to develop a different style of testing.


Fake[edit]

A fake is a type of test double that has a working implementation but is simplified and not suitable for production. Fakes are often used to simulate complex systems or dependencies in a controlled way, allowing tests to run quickly and deterministically.

Example: Imagine you have a class that interacts with a database. Instead of using a real database in your tests, you might use an in-memory database or a simple array to simulate the database operations.

class UserRepository {
    private $db;

    public function __construct(Database $db) {
        $this->db = $db;
    }

    public function findUserById($id) {
        return $this->db->query("SELECT * FROM users WHERE id = ?", [$id]);
    }
}

class FakeDatabase {
    private $data = [];

    public function query($query, $params) {
        // Simplified query logic for testing
        foreach ($this->data as $row) {
            if ($row['id'] == $params[0]) {
                return $row;
            }
        }
        return null;
    }

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

class UserRepositoryTest extends \PHPUnit\Framework\TestCase {
    public function testFindUserById() {
        $fakeDb = new FakeDatabase();
        $fakeDb->insert('users', ['id' => 1, 'name' => 'John Doe']);

        $repository = new UserRepository($fakeDb);
        $user = $repository->findUserById(1);

        $this->assertEquals('John Doe', $user['name']);
    }
}

getMockFromWsdl()[edit]

Although PHPUnit 9.x allows you to build a mock for a web service from a wp:WSDL file, the getMockFromWsdl() method is deprecated in current versions and removed in PHPUnit 12. This is because people don't use SOAP and WSDL as commonly as they used to, and even if they do, they don't rely on PHP's SOAP extension [4].

A bit sad, but understandable. PHPUnit can't keep 20 years of legacy implementations along with new technologies.

In PHPUnit 9.6, you can easily mock a Google Web Search[5]

References[edit]