PHPTesting Introduction

Testing Introduction

Every PHP script you've written so far has probably been tested the same way: run it, look at the output, and decide by eye whether it looks right. That works fine for a ten-line script. It falls apart fast once an application has dozens of functions, several collaborators editing the same codebase, and a deploy pipeline that runs several times a day. Automated testing replaces "look at it and see" with code that checks your code, so a broken assumption gets caught in seconds instead of surfacing as a bug report three weeks later.

This section introduces the ideas and tools behind testing PHP applications. It builds toward PHPUnit specifically, but the concepts — what to test, how much to test, and how to structure tests so they stay useful — apply no matter which framework you end up using.

Why manual testing doesn't scale

Manually testing a feature means opening a browser (or running a script) and clicking or typing through a scenario. It's fine as a sanity check while you're actively writing code, but it has three problems that get worse as a project grows:

  • It doesn't repeat itself. Every time you touch the code, someone has to redo the same manual steps, and busy people skip steps under deadline pressure.

  • It doesn't scale with the codebase. A change to a shared function might affect ten other features you didn't think to re-check.

  • It leaves no record. Six months later, nobody can tell whether a given edge case was ever verified, or by whom, or against what expected result.

Automated tests solve all three: they run identically every time, they run all of them (not just the ones you remembered), and the test suite itself is a living record of what the application is supposed to do.

The testing pyramid

Not all automated tests are the same shape. A useful mental model for a typical PHP web application is the testing pyramid: many small, fast tests at the bottom, and progressively fewer, slower, more realistic tests as you go up.

  • Unit tests — the base of the pyramid. Each one exercises a single function or class in isolation, with no database, no filesystem, and no network. A unit test for a function that calculates a shopping cart's total shouldn't need a running MySQL server; it just calls the function with sample data and checks the return value.

  • Integration tests — the middle layer. These check that two or more pieces work together correctly, for example that a repository class can actually read and write rows in a real (usually test-only) database, or that a service correctly calls an HTTP client.

  • End-to-end (E2E) tests — the top of the pyramid. These drive the whole application the way a user would: submitting a real HTTP request to a running app, or automating a browser click-through. They catch problems unit and integration tests can't, like a misconfigured route or a broken CSS selector, but they're slow and comparatively brittle.

The pyramid shape is a guideline, not a law: you want a large base of fast unit tests, a smaller number of integration tests around your riskiest boundaries (database queries, third-party APIs), and just enough end-to-end tests to confirm the critical user journeys — login, checkout, whatever matters most for your app — actually work when wired together.

An example: checking out a shopping cart

Imagine an e-commerce app with a function that calculates order totals, a repository class that saves orders to the database, and a checkout page that ties both together. Here's roughly how each layer of the pyramid would test it.

The function a unit test would target

PHP
<?php
function calculateOrderTotal(array $lineItems, float $taxRate): float
{
    $subtotal = array_sum(array_map(
        fn (array $item) => $item['price'] * $item['quantity'],
        $lineItems
    ));

    return round($subtotal * (1 + $taxRate), 2);
}

A unit test would call calculateOrderTotal() directly with a small array of line items and assert the returned float. An integration test would go a layer deeper, saving an order through the real repository class against a test database and confirming the row comes back with the correct total. An end-to-end test would submit an actual checkout form and assert the confirmation page shows the right amount. All three are testing related behavior, but at very different costs: the unit test runs in milliseconds, the integration test needs a database connection, and the end-to-end test needs the whole application running.

Coverage isn't the goal by itself
A test suite that reports "100% code coverage" can still miss real bugs if the assertions inside those tests are weak or the wrong behavior is being checked. Coverage tells you which lines executed during a test run, not whether the important scenarios were actually verified. Treat it as a signal for finding untested code, not a score to maximize.
What's ahead in this section

The rest of this section is a practical, hands-on path through testing a PHP application:

  • PHPUnit — installing the standard PHP testing framework and running your first test.

  • Writing Unit Tests — the Arrange-Act-Assert pattern, PHPUnit's assertion methods, and data providers for testing many inputs without duplicating code.

  • Mocking & Test Doubles — isolating the code under test from slow or unpredictable dependencies like databases and HTTP APIs.

  • Debugging (Xdebug) — stepping through code line by line when a test fails and it isn't obvious why.

  • Static Analysis (PHPStan/Psalm) — catching a whole category of bugs before any test even runs.

Tip
If your project has no tests yet, don't try to reach "full coverage" on day one. Start by writing a unit test for the function you're most afraid to change — the one where a mistake would be expensive or hard to notice. That single test earns its keep immediately and gives you a template to copy for the next one.