Skip to content

PHP Testing with PHPUnit — Complete Guide to Automated Testing

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about PHP Testing with PHPUnit. We cover key concepts, practical examples, and best practices to help you master this topic.

PHP testing with PHPUnit is how professional developers verify their code works correctly by writing automated tests that run assertions against functions and classes. This tutorial teaches you to install PHPUnit, write test cases, use data providers, mock dependencies, and measure Code Coverage so you can ship reliable PHP 8 applications with confidence.

What You'll Learn

  • Installing PHPUnit via Composer and configuring phpunit.xml
  • Writing test classes that extend TestCase and using setUp/tearDown
  • Using assertions like assertEquals, assertTrue, assertCount, and assertThrows
  • Creating data providers for parameterized tests
  • Using test doubles and mock objects to isolate dependencies
  • Measuring and interpreting code coverage reports

Why It Matters

Manual testing cannot keep up with the complexity of modern web applications. Every time you deploy a new feature, you risk breaking existing functionality. PHPUnit gives you a safety net: write tests once, run them in seconds, and catch regressions before they reach production. In team environments, tests also serve as living documentation that describes how each component should behave.

Real-World Use

The Durga Antivirus Pro team runs over 10,000 PHPUnit tests on every build. When a new malware signature parser is added, existing tests verify that file scanning, signature matching, and quarantine workflows still function correctly. Without automated tests, a single regression could ship a broken update to millions of users.

Learning Path

flowchart LR
  A[Dependency Injection] --> B[Testing with PHPUnit\nYou are here]
  B --> C[Debugging]
  style B fill:#f90,color:#fff

Installing PHPUnit

PHPUnit is installed as a Composer dev dependency. Open your terminal and run:

composer require --dev phpunit/phpunit ^11

After installation, create a phpunit.xml file in your project root:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
  <testsuites>
    <testsuite name="Application">
      <directory>tests</directory>
    </testsuite>
  </testsuites>
</phpunit>

Run the test suite with:

vendor/bin/phpunit

Output:

PHPUnit 11.0.0 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 00:00.015, Memory: 6.00 MB

OK (1 test, 1 assertion)

Writing Your First Test

Create a tests/ directory and add a test for a simple Calculator class.

First, the class under test in src/Calculator.php:

<?php
namespace App;

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

  public function divide(float $a, float $b): float
  {
    if ($b === 0.0) {
      throw new \InvalidArgumentException('Division by zero is not allowed');
    }
    return $a / $b;
  }
}

Now the test in tests/CalculatorTest.php:

<?php
namespace App\Test;

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

class CalculatorTest extends TestCase
{
  private Calculator $calculator;

  protected function setUp(): void
  {
    $this->calculator = new Calculator();
  }

  public function testAddReturnsCorrectSum(): void
  {
    $result = $this->calculator->add(2, 3);
    $this->assertEquals(5, $result);
  }

  public function testDivideReturnsCorrectQuotient(): void
  {
    $result = $this->calculator->divide(10, 2);
    $this->assertEquals(5, $result);
  }

  public function testDivideByZeroThrowsException(): void
  {
    $this->expectException(\InvalidArgumentException::class);
    $this->calculator->divide(10, 0);
  }
}

Output of vendor/bin/phpunit:

PHPUnit 11.0.0 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 00:00.018, Memory: 6.00 MB

OK (3 tests, 3 assertions)

Using Data Providers

Data providers let you run the same test with multiple inputs. Add this method to CalculatorTest.php:

<?php
namespace App\Test;

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

class CalculatorTest extends TestCase
{
  private Calculator $calculator;

  protected function setUp(): void
  {
    $this->calculator = new Calculator();
  }

  /** @dataProvider additionProvider */
  public function testAddWithMultipleInputs(float $a, float $b, float $expected): void
  {
    $this->assertEquals($expected, $this->calculator->add($a, $b));
  }

  public static function additionProvider(): array
  {
    return [
      [1, 1, 2],
      [2.5, 3.5, 6.0],
      [-1, 1, 0],
      [0, 0, 0],
      [100, 200, 300],
    ];
  }
}

Output:

PHPUnit 11.0.0 by Sebastian Bergmann and contributors.

........                                                               8 / 8 (100%)

Time: 00:00.020, Memory: 6.00 MB

OK (8 tests, 8 assertions)

Mocking Dependencies

When a class depends on an external service, mock that dependency to control its behavior. Consider a Mailer class that sends emails:

<?php
namespace App;

interface MailerInterface
{
  public function send(string $to, string $subject, string $body): bool;
}

class NewsletterService
{
  public function __construct(private MailerInterface $mailer) {}

  public function sendWelcome(string $email): bool
  {
    return $this->mailer->send(
      $email,
      'Welcome to the platform',
      'Thank you for signing up!'
    );
  }
}

Test the NewsletterService by mocking the MailerInterface:

<?php
namespace App\Test;

use App\MailerInterface;
use App\NewsletterService;
use PHPUnit\Framework\TestCase;

class NewsletterServiceTest extends TestCase
{
  public function testSendWelcomeReturnsTrueOnSuccess(): void
  {
    $mailer = $this->createMock(MailerInterface::class);
    $mailer->method('send')
      ->with('user@example.com', 'Welcome to the platform', 'Thank you for signing up!')
      ->willReturn(true);

    $service = new NewsletterService($mailer);
    $result = $service->sendWelcome('user@example.com');

    $this->assertTrue($result);
  }
}

Code Coverage

PHPUnit can measure which lines of your code are exercised by tests. Enable coverage in phpunit.xml:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
  <testsuites>
    <testsuite name="Application">
      <directory>tests</directory>
    </testsuite>
  </testsuites>
  <coverage>
    <include>
      <directory>src</directory>
    </include>
  </coverage>
</phpunit>

Run with coverage:

vendor/bin/phpunit --coverage-html coverage

Open coverage/index.html in a browser to see which methods are tested and which are not.

Common Mistakes

  1. Not using setUp for shared state: Creating fresh instances in setUp prevents test pollution. If one test modifies shared state, other tests may fail unpredictably.
  2. Ignoring the red-green-refactor cycle: Write a failing test first, make it pass, then refactor. Skipping the red step means you never verify the test can actually catch a failure.
  3. Testing implementation details: Test public behavior, not private methods. If you refactor internals, tests should still pass. Mock interfaces, not classes.
  4. Mocking everything: Over-mocking makes tests brittle and hard to read. Use real objects for value objects and simple classes; mock only expensive or unpredictable dependencies.
  5. Not running tests in CI: Tests that only pass on your local machine provide no safety net. Run PHPUnit in a continuous integration pipeline on every push.

Practice Questions

  1. What is the difference between assertEquals and assertSame in PHPUnit?

    • assertEquals checks value equality using ==, while assertSame checks identity with ===. Use assertSame when object identity or type strictness matters.
  2. How do you mark a test as skipped or incomplete?

    • Use $this->markTestSkipped('Reason') or $this->markTestIncomplete('Reason') inside the test method.
  3. What is the purpose of the @dataProvider annotation?

    • It allows a test method to receive multiple sets of arguments from a separate provider method, eliminating repetitive test code.
  4. How do you test that an exception is thrown?

    • Call $this->expectException(ExceptionClass::class) before the code that should throw.
  5. Challenge: Write a test suite for a UserValidator class that checks email format, password length (minimum 8 characters), and username uniqueness against a Repository. Use mocks for the repository.

Mini Project

Create a PHPUnit test suite for a simple shopping cart application:

  • Write an Item class with name, price, and quantity properties.
  • Write a Cart class that can add items, remove items, calculate the total, and apply a discount.
  • Write tests covering: adding items, removing items, empty cart total, discount calculation, negative quantity rejection, and duplicate item handling.
  • Measure code coverage and aim for 100% line coverage.

FAQ

What is PHPUnit and why should I use it?

PHPUnit is the de facto testing framework for PHP. It provides assertions, mock objects, and code coverage analysis to help you write reliable, maintainable code.

How do I install PHPUnit?

Install it via Composer: composer require --dev phpunit/phpunit ^11. Create a phpunit.xml configuration file and run vendor/bin/phpunit.

What is a test double?

A test double replaces a real object with a fake one during testing. Mocks, stubs, and spies are all types of test doubles used to isolate the code under test.

Does PHPUnit work with Laravel?

Yes. Laravel extends PHPUnit with convenience methods for HTTP testing, database assertions, and mocking facades. Run php artisan test to use the Laravel wrapper.

What is code coverage?

Code coverage measures what percentage of your source code is executed by tests. PHPUnit generates HTML reports so you can see which lines are covered and which are not.

Can I test private methods?

You should test private methods indirectly through public methods. If a private method is complex enough to need direct testing, consider extracting it into a separate class.

What's Next

Now that you can write tests, learn how to debug failing code efficiently in the next lesson on Debugging.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro