Lesson 12.2: Writing a test
A test is the cheapest way to know a change did not break something, and the best documentation of what code is meant to do. In this lesson learners write a small PHPUnit test for the template operator of lesson 7.5, run it with the kernel's test bootstrap, and see it catch a mistake.
At a glance
| Course | Developers |
| Module | Module 12: Tests and quality |
| Duration | 50 minutes (15 presenting, 35 exercise) |
| Level | Intermediate |
| Audience | Developers |
| Prerequisites | Lesson 12.1 and Lesson 7.5 |
Learning objectives
After this lesson, learners can:
- write a test class extending
PHPUnit\Framework\TestCasewith test methods and assertions; - test code without a database by calling it directly;
- run one test file with
vendor/bin/phpunit --bootstrap tests/bootstrap.php <file>; - read a failure message and fix the code or the test;
- choose what deserves a test: behavior others rely on, and every bug fixed.
Trainer's notes
How to present it
Sources: Testing and continuous integration and a kernel test of a
template operator, for example in tests/tests/kernel/common/.
- Anatomy (4 minutes). A class named
<Something>Test, extendingTestCase; public methods starting withtest; each prepares input, calls the code and asserts the result withassertSame(),assertTrue(),assertCount()and others. - DB-less (3 minutes). The operator is plain PHP: call
modify()with a value and parameters, read the value back. No kernel boot, no database, milliseconds per test. - Where tests live (3 minutes). The kernel's tests are in
tests/tests/. An extension of your own keeps its tests in its own repository, for exampleextension/mysite_hello/tests/, and runs them with the kernel's bootstrap. - Red, then green (3 minutes). Break the code on purpose and watch the test fail with a clear message. A test that never failed has not proven anything.
- What to test (2 minutes). Behavior other code relies on, edge cases (empty input, multibyte text), and every bug: write the test that shows the bug before fixing it.
Common questions
What about code that needs the database? The kernel's toolkit has ezpDatabaseTestCase for tests tagged
@group database, run against a throwaway database with tests/runtests.php.
Should I test templates? Templates are checked with eztemplatecheck.php; logic belongs in PHP, where it can be
tested.
How many assertions per test? One behavior per test method; several assertions are fine when they describe one behavior.
Pitfalls
- Tests that depend on each other. Each test must work alone and in any order.
- Tests that need the live site. Use fixtures and stand-ins; never write tests that change real content.
- Testing the implementation, not the behavior. Assert the output, not private details, so refactoring does not break the test.
Hands-on exercise
On the development installation, from its root folder.
Create
extension/mysite_hello/tests/MysiteInitialsTest.php:/** * Tests of the template operator mysite_initials. */ use PHPUnit\Framework\TestCase; require_once __DIR__ . '/../autoloads/mysitehellooperators.php'; class MysiteInitialsTest extends TestCase { private function initials( $text, $separator = '' ) { $operator = new mysiteHelloOperators(); $value = $text; $operator->modify( null, 'mysite_initials', array(), '', '', $value, array( 'separator' => $separator ) ); return $value; } public function testTwoWords() { $this->assertSame( 'AL', $this->initials( 'Ada Lovelace' ) ); } public function testSeparatorAndLowerCase() { $this->assertSame( 'G.B.H', $this->initials( 'grace brewster hopper', '.' ) ); } public function testEmptyText() { $this->assertSame( '', $this->initials( ' ' ) ); } public function testMultibyte() { $this->assertSame( 'ÉÖ', $this->initials( 'élan öl' ) ); } }Run it:
vendor/bin/phpunit --bootstrap tests/bootstrap.php extension/mysite_hello/tests/MysiteInitialsTest.php.Break the operator on purpose: in
autoloads/mysitehellooperators.phpreplacemb_strtoupper( mb_substr(withstrtoupper( substr((keep the brackets), and run the test again.Read which test fails and why; restore the code and run again.
Add a test of your own for two spaces between words (
'Ada Lovelace'). Decide what the result should be, then run it.Commit the test.
Expected result
- Step 2:
OK (4 tests, 4 assertions). - Step 3:
testMultibytefails: byte functions cut the multibyte letters, and the failure shows expected and actual values. - Step 4: all tests pass again.
- Step 5: a fifth test passes with
AL(the split on\s+handles several spaces). - The trainer asks each learner what their new test protects against.
Quiz
- Which class does a PHPUnit test extend?
- Which assertion compares type and value?
A.
assertEquals()B.assertSame()C.assertTrue()D.assertCount() - Why is the operator test fast?
- When should you write a test for a bug?
- Where does an extension of your own keep its tests?
Answers
PHPUnit\Framework\TestCase(or a toolkit class such asezpDatabaseTestCasefor database tests).- B.
assertSame(). - It calls plain PHP without booting the kernel or a database.
- Before fixing it: the test shows the bug, then proves the fix.
- In its own repository, for example
extension/mysite_hello/tests/.
Further reading
Related pages
- Module 12: Tests and quality
- Developers: course overview
- Trainers: start here
- Free documentation: this page is free to copy, translate and improve under the GNU FDL 1.3.
Become an Exponential Partner
Partners share their information, provide funding, contribute back and develop the products with us, and that is how Exponential grows. A partner website and dashboard are planned for mid-2027: please become a partner today to speed up our growth rate and land new features faster.