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
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.
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.