Phpunit Testing
By the end of this lesson you'll install PHPUnit with Composer and write real automated tests — assertions, data providers, setup/teardown, and mocks — so you can refactor and ship PHP with confidence instead of crossing your fingers.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free Php course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ Installing PHPUnit with Composer
PHPUnit is the standard testing framework for PHP — a tool that runs your tests and tells you which passed and which failed. You install it with Composer (PHP's package manager) as a dev dependency , meaning it's available while you build but never shipped to your live server. After installing, Composer places a runnable program at vendor/bin/phpunit .
One tiny config file tells the runner where your tests live, so you can just type ./vendor/bin/phpunit with no arguments. By convention, source code goes in src/ and tests go in tests/ .
2️⃣ Your First TestCase
A test is just a class that extends TestCase , with one or more public methods whose names start with test . Inside each method you follow Arrange-Act-Assert : build what you need, call the one thing you're testing, then check the result with an assertion . An assertion is a line that says "this had better be true" — if it isn't, the test fails. To check that code throws , you call expectException() before the line that should throw.
Read the output bottom-up: OK (2 tests, 2 assertions) means both methods ran and every assertion held. Each dot in .. is one passing test; a failure shows as F , an error as E .
3️⃣ The Assertions You'll Use Most
PHPUnit ships dozens of assertions, but a handful cover nearly everything. The key distinction is assertEquals versus assertSame : assertEquals compares values loosely (so 5 equals "5" ), while assertSame compares value and type strictly (like === ). Reach for assertSame by default — it catches accidental string-vs-int bugs. The exception is floats, where rounding means you use assertEqualsWithDelta .
4️⃣ setUp() and tearDown(): Fresh State Every Test
Tests must be isolated — each one runs as if it were the only test in the world. PHPUnit helps by calling setUp() before every test method , so you can build a fresh object there instead of repeating yourself. The matching tearDown() runs after every test to undo side effects (delete temp files, roll back a database). Because setUp() re-runs each time, no test can leak state into the next.
Now you try. The test class below is almost complete — fill in each ___ with the right assertion name using the 👉 hint, then run it and check it against the Output panel.
5️⃣ Data Providers: One Test, Many Inputs
When you want to test the same logic against lots of inputs — valid and invalid emails, edge-case numbers — don't copy-paste the test. A data provider is a static method that returns an array of rows ; each row is the arguments for one run. You attach it with the #[DataProvider('methodName')] attribute, and PHPUnit runs the test once per row , reporting each as its own pass or fail. Adding a new case becomes a one-line change.
Your turn again. Below, a plain test needs to become data-driven. Fill in the expected numbers and the missing attribute name.
6️⃣ Mocking: Faking the Database and the Mailer
Real dependencies — a database, an email server, a payment API — make tests slow, flaky, and dangerous (you don't want a test emailing real users). The fix is a test double : a stand-in object. A stub just returns canned values so your code has something to work with. A mock goes further and verifies interactions — that a method was called the right number of times with the right arguments. PHPUnit's createMock() builds one from a class or interface; expects($this->once()) asserts it's called exactly once.
Notice the second test uses expects($this->never()) to prove a negative : a duplicate signup must not send a welcome email. That's behaviour you simply can't check by inspecting a return value — only a mock can assert it.
7️⃣ Running the Suite and Reading a Failure
You run everything with ./vendor/bin/phpunit tests/ . While debugging, narrow it down: ./vendor/bin/phpunit tests/CalculatorTest.php runs one file, and --filter testAddsTwoNumbers runs one method. When a test fails, PHPUnit prints the class, the method, what it expected vs what it got , and the exact file and line — read it carefully before touching code.
📋 Quick Reference — PHPUnit
No test code is filled in this time — just the class under test, a brief, and an outline. Write the test class yourself, run it with ./vendor/bin/phpunit , then check your result against the expected output in the comments. This is exactly the write-run-check loop you'll use on every real project.
Practice quiz
How do you install PHPUnit so it never ships to production?
- composer require phpunit/phpunit
- pecl install phpunit
- composer require --dev phpunit/phpunit
- apt install phpunit
Answer: composer require --dev phpunit/phpunit. The --dev flag adds PHPUnit as a dev-only dependency, available while you build but kept out of your live server.
What must a PHPUnit test class do, and how are test methods named?
- Extend TestCase; public methods whose names start with 'test'
- Implement Testable; methods end in Test
- Extend Assert; methods named check*
Answer: Extend TestCase; public methods whose names start with 'test'. A test class extends PHPUnit's TestCase and has public methods whose names start with 'test'.
What is the difference between assertEquals and assertSame?
- assertEquals is strict; assertSame is loose
- They are identical
- assertSame only works on arrays
- assertEquals compares values with coercion; assertSame compares value AND type strictly
Answer: assertEquals compares values with coercion; assertSame compares value AND type strictly. assertEquals(5, "5") passes (loose), but assertSame(5, "5") fails because assertSame checks value and type like ===.
Which assertion should you use for floating-point comparisons?
- assertSame
- assertEqualsWithDelta
- assertTrue
- assertCount
Answer: assertEqualsWithDelta. Rounding makes assertSame(0.3, 0.1 + 0.2) fail, so use assertEqualsWithDelta with a small delta for floats.
How do you assert that code throws an exception?
- Call expectException(X::class) before the line that should throw
- Wrap it in try/catch and call fail()
- Use assertThrows()
- Return the exception from the test
Answer: Call expectException(X::class) before the line that should throw. Call $this->expectException(InvalidArgumentException::class) before the call; the test fails if it never throws.
When does setUp() run?
- Once before the whole test class
- After every test method
- Before every test method, giving each a fresh state
- Only when a test fails
Answer: Before every test method, giving each a fresh state. setUp() runs before every test method, so building a fresh object there means no test can leak state into the next.
What is a data provider used for?
- Connecting to a database in tests
- Running the same test once per row of input/expected arguments
- Mocking a mailer
- Cleaning up after a test
Answer: Running the same test once per row of input/expected arguments. A data provider is a static method returning rows of arguments; #[DataProvider('name')] runs the test once per row.
What is the difference between a stub and a mock?
- A stub verifies calls; a mock returns canned values
- They are the same; only the name differs
- A mock can only replace databases
- A stub returns canned values; a mock also verifies interactions (how it was called)
Answer: A stub returns canned values; a mock also verifies interactions (how it was called). A stub just returns canned values; a mock additionally asserts interactions, e.g. expects($this->once())->method('send').
Which call builds a configurable test double you can add expectations to?
- $this->createStub()
- $this->createMock()
- new RealClass()
- $this->fake()
Answer: $this->createMock(). createMock() builds a double you can configure with willReturn and verify with expects(); createStub() makes a pure stub.
How do you run a single test file with PHPUnit?
- ./vendor/bin/phpunit --only tests/CalculatorTest.php
- phpunit run CalculatorTest
- ./vendor/bin/phpunit tests/CalculatorTest.php
- ./vendor/bin/phpunit --file=CalculatorTest
Answer: ./vendor/bin/phpunit tests/CalculatorTest.php. Pass the file path to run one file; use --filter testName to run a single method.