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.
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.
Common Errors (and the fix)
- Your tests pass, then a harmless refactor breaks dozens of them — you tested the implementation , not the behaviour . Asserting that a specific private method ran ties the test to how the code works today. Assert the public result (the return value, the thrown exception, the email sent) so you're free to rewrite the internals.
- "Failed asserting that null is true" — but only sometimes — your tests aren't isolated : one test leaves data behind and another depends on it. Build fresh state in setUp() and clean up in tearDown() ; never let one test rely on another running first.
- "Failed asserting that '5' is identical to 5" — you used assertSame where the value is the right number but the wrong type (a string from a form, say). Either cast the value, or use assertEquals if loose comparison is genuinely what you mean. For floats, switch to assertEqualsWithDelta — rounding makes assertSame(0.3, 0.1 + 0.2) fail.
- The whole suite takes 40 seconds and everyone stops running it — your "unit" tests are hitting a real database or network. That's slow and flaky. Replace the dependency with a createMock() double so the test runs in milliseconds, and keep the few genuine database tests in a separate, slower integration suite.
Pro Tips
- 💡 One behaviour per test. If a test name needs "and" in it ( testAddsAndRemoves ), it's probably two tests. Small tests pinpoint failures.
- 💡 Default to assertSame . Strict comparison catches a whole class of type bugs that assertEquals waves through.
- 💡 Follow the test pyramid: many fast unit tests, fewer integration tests, a handful of slow end-to-end tests. Run unit tests on every save.
📋 Quick Reference — PHPUnit
Syntax
Example
What It Does
assertSame
$this->assertSame(5, $x);
Equal value AND type (strict)
assertEquals
$this->assertEquals(5, $x);
Equal value (loose, type-coerced)
assertTrue
$this->assertTrue($ok);
Value is exactly true
expectException
$this->expectException(X::class);
The next call must throw X
setUp / tearDown
protected function setUp()
Runs before / after each test
#[DataProvider]
#[DataProvider('cases')]
Run a test once per data row
createMock
$this->createMock(Repo::class);
Build a fake dependency
vendor/bin/phpunit
./vendor/bin/phpunit tests/
Run the test suite
Frequently Asked Questions
Mini-Challenge: Test a BankAccount
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.
🎉 Lesson Complete!
- ✅ Install PHPUnit with composer require --dev phpunit/phpunit and run it from vendor/bin/phpunit
- ✅ A test is a class that extends TestCase with public test* methods, each following Arrange-Act-Assert
- ✅ assertSame is strict, assertEquals is loose; expectException checks throws
- ✅ setUp() and tearDown() give every test fresh, isolated state
- ✅ A data provider runs one test against many input rows via #[DataProvider]
- ✅ createMock() replaces databases and mailers so tests stay fast and isolated
- ✅ Next lesson: Composer Packages — bundle and publish reusable PHP libraries (now with a test suite behind them)
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.
Continue this course
- Previous: Performance
- Next: Composer Packages