Home / News / Module 3: Deepening Your Testing Knowledge

Module 3: Deepening Your Testing Knowledge

Test Doubles: Understanding Mocks and Stubs

Test Doubles, also known as Mocks and Stubs, are an essential aspect of PHP unit testing. They allow you to create fake objects that behave like real ones, providing a way to test the behavior of your code without actually interacting with the real system. This approach makes your tests more flexible and allows you to isolate your code and test individual units without affecting the core functionality of your application.

Here’s how you can implement Mocks and Stubs in your PHP unit tests:

1. **Create a Mock Object**: A Mock object is an instance of a real class that behaves differently than the real class being tested. It’s created using the Mock class from the PHPUnit\Framework\TestCase class. Here’s an example:
“`php
mockCalculator = $this->getMockBuilder(Calculator::class)
->disableOriginalConstructor()
->getMock();
}

public function testSimpleSum(): void

Module 3: Deepening Your Testing Knowledge

You have now mastered the basics: you know how to set up PHPUnit, write test classes, and use the most important assertions. Now, let’s deepen our knowledge with techniques that will make your tests more efficient, organized, and powerful. In this module, you will learn how to run the same test with multiple datasets, how to prepare the “stage” for your tests, and how to organize your test suite professionally.

1. Data Providers: Test More with Less Code

Imagine you need to test a sum function with various combinations of numbers: positive, negative, zeros, etc. You could write a test method for each combination, but that would lead to a lot of code duplication.

This is exactly the problem that Data Providers solve. A data provider is a public method in your test class that returns an array of arrays. Each of these inner arrays contains the arguments for one execution of your test method.

How It Works:

  1. Create a test method that accepts arguments.
  2. Create a public method that returns an array of arrays (or an Iterator object). This is your data provider.
  3. Annotate the test method with the #[DataProvider('method')] tag.

Practical Example:

Let’s test a simple Calculator class.

// src/Calculator.php
namespace App;

class Calculator
{
    public function sum(int $a, int $b): int
    {
        return $a + $b;
    }
}

Now, the test with a data provider:

// tests/CalculatorTest.php
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use App\Example;

class CalculatorTest extends TestCase
{
    #[DataProvider('sumProvider')]
    public function testSumWithDifferentValues(int $a, int $b, int $expected)
    {
        $calculator = new Calculator();
        $result = $calculator->sum($a, $b);
        $this->assertEquals($expected, $result);
    }

    public static function sumProvider(): array
    {
        return [
            'Sum of two positives'      => [2, 3, 5],
            'Sum of positive with zero' => [5, 0, 5],
            'Sum of two negatives'      => [-2, -3, -5],
            'Sum of positive with negative' => [5, -3, 2],
        ];
    }
}

Analyzing the Example:

  • **use PHPUnit\Framework\Attributes\DataProvider;**: Imports the DataProvider attribute, which is a modern way (PHP 8+) of connecting a test method to a data provider.
  • #[DataProvider('sumProvider')]: This attribute is the central piece. It tells PHPUnit: “To run this test, first call the sumProvider() method to get a set of data. Then, execute this test method once for each data set it provides.”
  • public function testSumWithDifferentValues(int $a, int $b, int $expected): This is the test method itself. The parameters $a, $b, and $expected will receive their values from the sumProvider.
  • he **sumProvider** method returns an array. Each item in this array is another array containing the data for one test run: [$a, $b, $expected].
  • $this->assertEquals($expected, $result);: This is the assertion (the check). It compares if the expected result ($expected) is equal to the actual result ($result). If they are equal, the test for that data set passes ✅. If not, it fails ❌.

When you run this test, PHPUnit will execute testSumWithDifferentValues four times, once for each dataset provided by sumProvider.

2. Fixtures and the Test Lifecycle: setUp() and tearDown()

In many tests, you need to perform some setup actions before running the test logic and some cleanup actions afterward. For example, you might need to create an object instance before each test or clear a log file after execution. This predefined environment and state is called a fixture.

PHPUnit provides two special methods to manage your test’s lifecycle and ensure that each test runs in a clean, isolated environment:

  • setUp(): This method is executed before each test method (test*) in the class. It’s the perfect place to instantiate objects or prepare the environment.
  • tearDown(): This method is executed after each test method. It’s used to “clean up the mess,” such as closing database connections or removing temporary files.

Practical Example:

Let’s refactor our CalculatorTest to use setUp().

// tests/CalculatorTest.php
<?php

use PHPUnit\Framework\TestCase;
use App\Calculator;

class CalculatorTest extends TestCase
{
    private ?Calculator $calculator;

    protected function setUp(): void
    {
        // This line will run before EACH test in this class
        $this->calculator = new Calculator();
        echo "Running setUp()\n";
    }

    protected function tearDown(): void
    {
        // This line will run after EACH test in this class
        unset($this->calculator);
        echo "Running tearDown()\n";
    }

    public function testSimpleSum()
    {
        $result = $this->calculator->sum(1, 1);
        $this->assertEquals(2, $result);
    }

    public function testSumWithNegatives()
    {
        $result = $this->calculator->sum(-5, 2);
        $this->assertEquals(-3, $result);
    }
}

By using setUp(), we avoid writing $calculator = new Calculator(); in every test method, making our code cleaner (DRY principle – Don’t Repeat Yourself) and ensuring that each test gets a fresh instance of the object. This prevents the result of one test from interfering with another.

3. Organizing Your Tests: The phpunit.xml File

As your project grows, running tests from the terminal by specifying the directory every time becomes repetitive. PHPUnit can be configured through an XML file, usually named phpunit.xml or phpunit.xml.dist, in the root of your project.

This file allows you to define default settings for your test suite, such as:

  • The directory where the tests are located.
  • The “bootstrap” file (a file that is executed before the tests begin, perfect for loading the Composer autoloader).
  • Colors in the terminal output.
  • And many other advanced options we’ll see later, like filters and code coverage.

Creating a basic phpunit.xml:

Create the file phpunit.xml in your project’s root directory.

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true">

    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests</directory>
        </testsuite>
    </testsuites>

</phpunit>

Analyzing the File:

  • bootstrap="vendor/autoload.php": Essential. It ensures the Composer autoloader is loaded before any tests, so your classes can be found automatically.
  • colors="true": Makes the terminal output nicer, with green bars for success and red for failures.
  • <testsuites>: Defines one or more sets of tests.
  • <testsuite name="Unit">: Names our test suite “Unit”.
  • <directory suffix="Test.php">./tests</directory>: The most important directive. It tells PHPUnit to look for files ending with Test.php inside the ./tests directory.

Now, to run all the tests in your project, you just need to type in the terminal:

./vendor/bin/phpunit

PHPUnit will automatically find and use the phpunit.xml file, running your test suite with the defined configurations.

Next Steps:

You now know how to write more robust and organized tests. In the next module, we will dive into one of the most powerful and sometimes challenging concepts in the world of unit testing: Test Doubles, better known as Mocks and Stubs. This technique is fundamental for isolating your code and testing units that depend on other complex parts of the system.

Tagged:

Leave a Reply

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