Introduction: Why is testing needed in PHP?
\n\nDeveloping in PHP without testing is like building a house without a foundation. Sooner or later, the application will crash, and users will see the "white screen of death." Code testing is not a luxury, but a necessity for any serious project. It allows you to:
\n- \n
- Detect errors at early stages — before the code reaches production. \n
- Simplify refactoring — you can confidently change the architecture, knowing that tests will catch regressions. \n
- Create self-documenting code — tests serve as live documentation of how the system should work. \n
- Increase team confidence — every new commit undergoes automatic verification. \n
In PHP, there are three main levels of testing: Unit, Integration, and E2E (End-to-End). In this article, we will break down each of them with practical examples, from simple asserts to full-fledged browser tests.
\n\n1. Unit Testing: Testing the "Building Blocks" of the Application
\n\nWhat is a Unit Test?
\nA unit test checks the operation of one small block of code — typically one method or function — in complete isolation from external dependencies (databases, APIs, file systems).
\n\nTools for PHP
\n- \n
- PHPUnit — the de facto standard. Used in Laravel, Symfony, and many CMS platforms. \n
- Pest — a modern alternative with a more concise syntax, built on top of PHPUnit. \n
Example: Testing a Discount Calculator
\nSuppose we have a simple class DiscountCalculator:
<?php\n\nclass DiscountCalculator\n{\n public function calculate(float $price, float $percent): float\n {\n if ($percent < 0 || $percent > 100) {\n throw new \\InvalidArgumentException('Percent must be between 0 and 100');\n }\n return $price - ($price * $percent / 100);\n }\n}\n\n\nNow let's write a Unit test using PHPUnit:
\n\n<?php\n\nuse PHPUnit\\Framework\\TestCase;\n\nclass DiscountCalculatorTest extends TestCase\n{\n private DiscountCalculator $calculator;\n\n protected function setUp(): void\n {\n $this->calculator = new DiscountCalculator();\n }\n\n public function testCalculateWithZeroPercent(): void\n {\n $result = $this->calculator->calculate(100, 0);\n $this->assertEquals(100, $result);\n }\n\n public function testCalculateWithFiftyPercent(): void\n {\n $result = $this->calculator->calculate(200, 50);\n $this->assertEquals(100, $result);\n }\n\n public function testCalculateThrowsExceptionOnInvalidPercent(): void\n {\n $this->expectException(\\InvalidArgumentException::class);\n $this->calculator->calculate(100, 150);\n }\n}\n\n\nKey points:
\n- \n
- Each method tests only one scenario. \n
- We do not access the database or external services. \n
- We use
setUp()to initialize common objects. \n
Best Practices for Unit Testing
\n- \n
- Follow the FIRST principle: Fast, Isolated, Repeatable, Self-validating, Timely. \n
- Name tests as "test_action_expectedResult". \n
- Avoid logic in tests — no if/switch/loops. \n
2. Integration Testing: Checking Component Interaction
\n\nWhat is the difference from Unit?
\nIntegration tests check how different parts of the system work together. For example, how your service interacts with a real database, cache, or external API.
\n\nExample: Testing a Repository with a Database (Laravel + SQLite)
\nSuppose we have a <