Testing
A test is code that opens an application page by itself and checks the response: the status code, the matched route, text in the HTML. Running the tests after a change shows at once whether something broke elsewhere. Laminas uses PHPUnit and the laminas-test package for this.
Installation
composer require --dev laminas/laminas-testThe --dev flag means the package is needed only during development. The skeleton already has the phpunit.xml.dist config file and a sample test in module/Application/test.
Where tests live
A module's tests go into module/<Module>/test. Announce this folder in two places. In composer.json — the autoload-dev section:
{
"autoload-dev": {
"psr-4": {
"ApplicationTest\\": "module/Application/test/",
"BlogTest\\": "module/Blog/test/"
}
}
}In phpunit.xml.dist — one more directory in the test suite:
<testsuites>
<testsuite name="Application Test Suite">
<directory>./module/Application/test</directory>
<directory>./module/Blog/test</directory>
</testsuite>
</testsuites>Then run composer dump-autoload.
The first test
Create the file by hand:
// module/Blog/test/Controller/PostControllerTest.php
namespace BlogTest\Controller;
use Blog\Controller\PostController;
use Laminas\Test\PHPUnit\Controller\AbstractHttpControllerTestCase;
class PostControllerTest extends AbstractHttpControllerTestCase
{
protected function setUp(): void
{
$this->setApplicationConfig(
include __DIR__ . '/../../../../config/application.config.php'
);
parent::setUp();
}
public function testLoginPageOpens(): void
{
$this->dispatch('/login', 'GET');
$this->assertResponseStatusCode(200);
$this->assertControllerName(AuthController::class);
$this->assertMatchedRouteName('login');
}
}The setUp() method runs before every test and loads the application configuration. dispatch() sends a request inside PHP, without a web server, and the assert... methods check the result.
Running
vendor/bin/phpunitPHPUnit prints a dot for every passed test and an OK line at the end. If the page returns another code, a message says which test failed and what was expected.
Replacing services
A test does not need the real database. A service in the container is replaced with a mock — an object that returns data set in advance:
use Blog\Model\Post;
use Blog\Model\PostTable;
public function testViewShowsPost(): void
{
$post = new Post();
$post->exchangeArray(['id' => 1, 'title' => 'First post', 'body' => 'Text']);
$table = $this->createMock(PostTable::class);
$table->method('find')->willReturn($post);
$services = $this->getApplicationServiceLocator();
$services->setAllowOverride(true);
$services->setService(PostTable::class, $table);
$this->dispatch('/blog/1');
$this->assertResponseStatusCode(200);
$this->assertStringContainsString('First post', $this->getResponse()->getContent());
}setAllowOverride(true) allows replacing a service that is already defined. The controller receives the mock instead of PostTable, and no database query runs.
Common assertions
| Method | What it checks |
|---|---|
assertResponseStatusCode(200) | The response code |
assertMatchedRouteName('blog') | The matched route |
assertControllerName($class) | The controller from the route |
assertActionName('index') | The action |
assertRedirectTo('/blog') | A redirect to an address |
A form submission is tested with the third argument of dispatch(): $this->dispatch('/blog/add', 'POST', ['title' => 'Test']).
Common mistake
Tests that work with the developer's real database spoil data and pass differently on different machines. Replace table classes with mocks, as in the example above, or set up a separate test database and name it in configuration that is loaded only in tests.