How to Test With PHP

Note: This article is written for web publishing and synthesizes current, real-world PHP testing practices from official PHP, PHPUnit, Laravel, Symfony, Pest, Composer, Xdebug, Codeception, Mockery, PHPStan, Psalm, and continuous integration documentation. Source links are intentionally not displayed to keep the article clean for publication.

Testing PHP code is a little like checking your parachute before jumping out of the plane. Technically, you could skip it. Emotionally, please do not. Whether you are building a tiny contact form, a Laravel application, a Symfony API, a WordPress plugin, or a custom PHP backend that bravely talks to five databases and one mysterious legacy server named “production-old-final-v3,” testing helps you prove that your code does what you think it does.

The good news is that modern PHP testing is no longer the scary basement of web development. Tools such as PHPUnit, Pest, Codeception, Mockery, Xdebug, PHPStan, and Psalm make it practical to test business logic, HTTP responses, database behavior, APIs, commands, and edge cases. The even better news: you do not need to test everything on day one. You need a smart, repeatable testing habit that grows with your project.

This guide explains how to test with PHP in a practical, beginner-friendly, and professional way. You will learn what PHP testing means, how to set up PHPUnit, how to write useful unit tests, when to use integration and feature tests, how to mock dependencies, how to check test coverage, and how to avoid the classic “my tests pass but my app is still broken” situation. Yes, it happens. No, the tests will not apologize.

What Does It Mean to Test With PHP?

To test with PHP means to run automated checks against your PHP code so you can confirm that functions, classes, routes, services, and application behavior produce the expected results. Instead of manually clicking through your website every time you make a change, you write test code that checks your real code for you.

For example, suppose you have a function that calculates a discount. You could open the browser, place an order, enter a coupon, and inspect the result manually. Or you could write a PHP unit test that says, “When the cart total is $100 and the discount is 10%, the final price should be $90.” The computer then runs that test every time you ask it to. Computers love repetitive chores. That is basically their whole brand.

Why PHP Testing Matters

PHP powers a massive amount of web software, from small business websites to large platforms, e-commerce systems, content management systems, APIs, dashboards, and internal tools. As projects grow, small changes can break surprising things. A harmless-looking edit to a payment calculation might damage invoices. A tiny route change might break an API client. A database migration might turn a working feature into a very expensive loading spinner.

Automated PHP testing helps you catch problems earlier, document expected behavior, refactor with confidence, and onboard new developers faster. Tests also reduce fear. Without tests, refactoring old PHP code can feel like defusing a bomb while wearing oven mitts. With tests, you still need skill, but at least you have a warning system.

Main Types of PHP Tests

Unit Tests

Unit tests check small, isolated pieces of code. Usually, a unit test focuses on one method, one function, or one class. These tests should be fast, focused, and easy to understand. A good unit test does not need a database, a web server, or a third-party API. It should be able to run quickly from the command line.

Example use cases for PHP unit testing include testing price calculations, string formatting, validation rules, date handling, permission logic, and service methods. If a piece of code contains business logic, it is usually a good candidate for a unit test.

Integration Tests

Integration tests check whether multiple parts of your application work together. For example, you may test whether a repository class can save and retrieve data from a database, whether a mail service receives the correct payload, or whether an API client correctly handles a response.

Integration tests are slower than unit tests because they involve real dependencies, but they are extremely valuable. Many bugs do not live inside one method. They live in the awkward handshake between two pieces of code that both insist they are innocent.

Feature and HTTP Tests

Feature tests check application behavior from a user or client perspective. In Laravel, for example, HTTP tests can make requests to your application and assert that the response status, content, redirects, session data, or database state is correct. Symfony also supports application-level tests built around PHPUnit and its testing tools.

Feature tests are perfect for checking login flows, registration forms, checkout behavior, API endpoints, admin actions, and permission rules. They do not usually test every tiny method. Instead, they answer the practical question: “Does this feature work when someone actually uses it?”

Acceptance and Browser Tests

Acceptance tests simulate real user behavior. They may click buttons, fill forms, follow links, and verify what appears on the page. Tools such as Codeception and browser testing solutions can help test full user journeys.

These tests are powerful but slower and more fragile than unit tests. Use them for critical flows, not every possible screen. Your test suite should not require a coffee break, a lunch break, and a motivational speech to finish.

Static Analysis

Static analysis is not a traditional test, but it belongs in every serious PHP testing workflow. Tools such as PHPStan and Psalm inspect your code without running it. They can catch type errors, impossible conditions, missing methods, incorrect return values, and suspicious assumptions.

Think of static analysis as a very strict editor who reads your PHP code and says, “Are you absolutely sure this variable is not null?” Annoying? Sometimes. Useful? Very.

Best PHP Testing Tools

PHPUnit

PHPUnit is the classic and widely used testing framework for PHP. It follows the xUnit testing style and is commonly used for unit tests, integration tests, and framework-based testing. Modern PHPUnit tests usually extend PHPUnit\Framework\TestCase, and test methods often begin with test or use attributes to mark test methods.

If you want to learn PHP testing seriously, start with PHPUnit. Even if you later use Pest, Laravel testing helpers, Symfony testing tools, or Codeception, understanding PHPUnit gives you a strong foundation.

Pest

Pest is a modern PHP testing framework focused on simplicity and developer experience. It provides a clean, expressive syntax and works well with many PHP projects. Pest is especially popular in the Laravel community, but it can be used beyond Laravel too.

PHPUnit feels like a formal meeting. Pest feels like the same meeting after someone brought snacks. Both can be professional; one is simply more relaxed.

Codeception

Codeception is a full-stack PHP testing framework that supports unit, functional, and acceptance testing. It is especially useful when you want readable tests that describe user actions clearly. For web applications, Codeception can help structure tests around what users actually do.

Mockery

Mockery is a PHP mock object framework used with PHPUnit and other testing tools. It helps create test doubles such as mocks, stubs, and spies. These are useful when you want to isolate the code being tested from external dependencies such as APIs, mailers, payment gateways, or slow services.

Xdebug and PCOV

Xdebug and PCOV are commonly used for code coverage in PHP. Code coverage reports show which lines of code were executed while your tests ran. Coverage is helpful, but it is not magic. A line can be executed without being meaningfully tested. Coverage tells you where your tests went; it does not guarantee they asked intelligent questions while visiting.

How to Set Up PHPUnit in a PHP Project

The usual way to install PHPUnit is through Composer as a development dependency. In most modern PHP projects, Composer manages dependencies and autoloading.

After installation, PHPUnit can usually be run from the vendor binary directory:

On Windows, the command may look like this:

A common project structure looks like this:

The src directory contains application code. The tests directory contains test code. The phpunit.xml file stores PHPUnit configuration, such as test directories, bootstrap files, and coverage settings.

A Simple PHPUnit Example

Let’s create a basic class. This example is intentionally simple because testing should begin with something understandable before you invite databases, queues, and “temporary” legacy code to the party.

Now write a test for it:

This test creates a Calculator, calls the add method, and checks that the result is exactly 5. The assertion assertSame checks both value and type, which makes it stricter than assertEquals. In PHP, where values can sometimes change type more casually than a teenager changes playlists, strict assertions are often your friend.

How to Write a Good PHP Test

A useful PHP test usually follows the Arrange, Act, Assert pattern:

  • Arrange: Prepare the object, data, or environment.
  • Act: Run the method or behavior being tested.
  • Assert: Check that the result is correct.

Here is another example:

And the test:

Notice that the second test checks failure behavior. Good testing does not only confirm that happy paths work. It also checks what happens when someone enters a bad value, a missing value, a weird value, or a value that looks like it came from a raccoon walking across the keyboard.

Testing PHP Exceptions

Many PHP applications throw exceptions when something invalid happens. Your tests should confirm those exceptions. This is especially important for validation, permissions, payment rules, file handling, and API clients.

Use exception tests when your code is supposed to reject bad input. Do not treat exceptions as failures if they are the expected behavior. A test that proves your code refuses nonsense is a good test.

Testing With Data Providers

When the same behavior needs to be tested with multiple inputs, data providers keep your tests clean. Instead of writing ten nearly identical tests, you provide several input-and-output sets.

Data providers are excellent for validation rules, calculations, formatters, and edge cases. They also reduce copy-and-paste testing, which is good because copy-and-paste bugs reproduce faster than rabbits with Wi-Fi.

Mocking Dependencies in PHP Tests

Real applications depend on many outside services: databases, mail servers, APIs, file systems, queues, cache layers, and payment processors. Unit tests should usually avoid calling those services directly. Instead, use mocks or stubs.

A stub returns prepared data. A mock can also verify that a method was called in a specific way. For example, if you test an order service, you may not want to send a real email. You only want to confirm that your mailer was asked to send the correct message.

A PHPUnit-style mock may look like this:

This test does not send an actual email. It confirms that the service interacts with the mailer correctly. That is faster, safer, and less likely to accidentally email your entire customer list with “test test please ignore.”

Testing PHP Applications With a Database

Database testing is useful, but it needs discipline. You should not run tests against your production database. That sentence deserves its own chair, spotlight, and dramatic music: never run automated tests against production data.

Use a separate testing database, transactions, migrations, factories, or fixtures. Frameworks such as Laravel provide convenient tools for refreshing databases and creating model data. Symfony projects often use test environments, test containers, and fixtures to prepare predictable data.

Database tests should check important persistence behavior, such as whether a user can be created, whether an order stores the correct totals, whether soft deletes work, or whether a query returns the expected records. Keep these tests focused. If every test needs the entire database universe to exist first, your suite will become slow and grumpy.

Testing APIs and HTTP Responses

For web applications, HTTP tests are essential. They check whether routes return the correct status codes, JSON structures, redirects, headers, and content. For APIs, you may test successful responses, validation errors, authentication failures, pagination, and authorization rules.

A Laravel-style HTTP test might check that an endpoint returns a successful response:

For JSON APIs, test the shape and meaning of the response. Do not only check that the server returned 200 OK. A page can say “OK” while quietly returning the wrong data, just like a person saying “I’m fine” while holding three coffees and staring into the distance.

Using Test Coverage the Right Way

Code coverage tells you which lines, branches, or paths were executed during your test run. PHPUnit can generate coverage reports when a coverage driver such as Xdebug or PCOV is installed and configured.

Coverage is useful for finding untested areas, but do not worship the percentage. A project with 95% coverage can still have weak tests if those tests barely assert anything. A smaller suite with strong assertions around critical business behavior may be more valuable than a giant suite that mostly checks whether objects exist.

A practical goal is to improve meaningful coverage over time. Start with important code: payments, authentication, permissions, calculations, data transformations, and anything that has broken before. Bugs from the past are excellent test ideas. They are basically your application leaving sticky notes that say, “Please do not let me do this again.”

Running PHP Tests in Continuous Integration

Continuous integration, often called CI, runs your tests automatically when code is pushed or a pull request is opened. GitHub Actions, GitLab CI, Bitbucket Pipelines, and other CI tools can install PHP, run Composer, execute PHPUnit or Pest, and report failures before code is merged.

A typical CI workflow for PHP includes these steps:

  • Check out the project code.
  • Install the correct PHP version and extensions.
  • Install Composer dependencies.
  • Run static analysis.
  • Run the test suite.
  • Optionally generate coverage reports.

CI is where testing becomes a team habit instead of a personal preference. It prevents “works on my machine” from becoming the official company motto.

Common PHP Testing Mistakes

Testing Implementation Instead of Behavior

Good tests care about what the code does, not every private detail of how it does it. If your tests break every time you refactor cleanly, they may be too attached to implementation details.

Writing Tests That Are Too Large

A test should have a clear reason to exist. If one test checks login, checkout, email delivery, PDF generation, and inventory updates, it is not a test. It is a soap opera.

Ignoring Edge Cases

Do not only test normal input. Test empty strings, zero values, maximum values, invalid IDs, expired dates, missing permissions, duplicate records, and failed external responses.

Depending on Test Order

Each test should be able to run independently. If Test B only passes after Test A runs first, your suite is fragile. Tests should clean up after themselves or use isolated environments.

Skipping Static Analysis

PHP is flexible, which is wonderful until flexibility becomes “surprise, this value is null.” Static analysis catches many issues before runtime and works beautifully beside automated tests.

A Practical PHP Testing Workflow

Here is a simple workflow for testing PHP projects:

  1. Install PHPUnit or Pest as a development dependency.
  2. Create a tests directory.
  3. Write unit tests for important business logic.
  4. Add integration tests for database and service behavior.
  5. Add feature or HTTP tests for important user flows.
  6. Use mocks for external dependencies.
  7. Add PHPStan or Psalm for static analysis.
  8. Run tests locally before committing code.
  9. Run tests automatically in CI.
  10. Improve coverage gradually, especially around risky areas.

This workflow is realistic because it does not demand perfection immediately. It builds confidence step by step. Testing is not a one-day makeover. It is more like brushing your teeth: small, regular effort prevents painful surprises later.

Real-World Experience: Lessons From Testing PHP Projects

One of the biggest lessons from testing PHP is that the first tests are usually the hardest. Not because PHPUnit is impossible, but because old code often was not written with testing in mind. You may find classes that create their own database connections, call APIs directly, echo output in the middle of business logic, or depend on global state. This does not mean the project is doomed. It means the first job is often to create seams in the code.

A seam is a place where you can separate one part of the system from another. For example, instead of creating a mailer inside a service, inject the mailer through the constructor. Instead of calling a payment API directly from a controller, move that behavior into a payment service that can be mocked. These changes make code easier to test and usually easier to maintain. The funny part is that testing often improves design before it even catches a bug.

Another practical experience: start by testing code that makes money, loses money, blocks users, or causes support tickets. A perfect test suite for harmless formatting functions is nice, but if your checkout discounts are broken, nobody will praise your beautifully tested comma helper. Prioritize payment totals, subscriptions, login, permissions, imports, exports, and API integrations. Test the scary stuff first.

It is also wise to write tests for bugs after you fix them. When a bug appears, first reproduce it manually. Then write a failing test that proves the bug exists. After that, fix the code and watch the test pass. This turns a painful bug into permanent protection. Without that test, the same bug may return later wearing fake glasses and pretending to be new.

In team projects, readable test names matter more than people expect. A method named testItWorks is not helpful. What works? Why? Under what condition? A better name is testGuestCannotAccessAdminDashboard or testExpiredCouponCannotBeApplied. Good test names explain business rules. They become living documentation for future developers, including future you, who may remember absolutely nothing about why that code exists.

Speed matters too. If the test suite is slow, developers stop running it. Keep unit tests fast. Separate slower integration and browser tests when necessary. Run quick checks during local development and deeper checks in CI. A fast feedback loop encourages testing; a slow one encourages snacks, distractions, and suspiciously long “quick breaks.”

Finally, do not chase 100% coverage like it is a golden trophy guarded by dragons. High coverage can be useful, but meaningful assertions are more important. A test that executes code without checking the result is not very valuable. The best PHP tests are clear, focused, repeatable, and connected to real business risk. They help you move faster because they reduce the fear of breaking things. That is the real reward: not a perfect number, but the confidence to improve your application without whispering “please don’t explode” every time you deploy.

Conclusion

Learning how to test with PHP is one of the best investments you can make as a developer. Start with PHPUnit or Pest, write focused unit tests, add integration and feature tests where they matter, use mocks for external services, check coverage wisely, and run everything through continuous integration. Testing does not make your code perfect, but it makes your project safer, clearer, and easier to improve.

The secret is not to test everything immediately. The secret is to test the right things consistently. Begin with important business logic, protect old bugs from returning, and build a testing workflow your team can actually maintain. Your future self will thank you, probably while deleting less broken code.