Testing controllers in CakePHP
Testing a controller at first seems and is very simple. You send a request to the same URL that you would open up in the browser and check things like view variables (variables that are given to the view through the controller), if the request got the correct response and sometimes if the action caused the correct result (like adding an entry or something like that).
It gets significantly harder when Authentication is implemented.
Authentication causes you to have to fake a login. Faking a login can be rather difficult as it requires:
- Having a fixture entry for a User with valid permissions
- Preparing session data by getting that certain entry from the test database
- Adding the whole user entity we got in the previous step to the session with an “Auth” prefix
Kinda looks like this:
[ // Superuser 'id' => 'dd137c92-9f03-4f0d-8e9a-0fba608ed373', 'username' => 'superadmin', 'password' => '$2y$10$tJek1ncybnTlHjEeyFh22umgK9c145mI5t8nMd05WIakA3CtsvTCu', 'active' => 1, 'is_superuser' => 1, 'created' => '2026-08-25 11:21:37', 'modified' => '2026-08-25 11:21:37', ],
protected function getSuperuserSessionData(): Entity { /** @var UsersTable $usersTable */ $usersTable = $this->getTableLocator()->get('Users'); $sessionData = $usersTable->find()->where(['id' => '<id of the user entry>'])->first(); return $sessionData; }
protected function loginAs($sessionData): void { if (!method_exists($this, 'session')) { throw new RuntimeException('No session component available'); } $this->session([ 'Auth' => $sessionData, ]); }
Only then can you request the controller like normal.
When controller actions include saving things, it makes sense to compare the expected value to the view variable and to the value that was saved to the database.
This would kinda look like this for an index endpoint for accounts table:
public function testIndex(): void { $accountsTable = $this->getTableLocator()->get('Accounts'); $accountCount = $accountsTable->find()->count(); $this->loginAsSuperuser(); $this->get('/accounts'); $this->assertResponseOk(); $accounts = $this->viewVariable('accounts')->toArray(); $account = simplifyDateTimesInArray($accounts[0]->toArray()); self::assertCount($accountCount, $accounts); self::assertEquals([ 'id' => 1, 'uuid' => '9841e242-4ad6-43e7-842c-aeb392a0f862', 'created' => '2025-11-24 14:56:09.000000 CET', 'modified' => '2025-11-24 14:56:09.000000 CET', 'customer_id' => 1, 'name' => 'OSe', 'email' => 'isp@orca-services.ch', 'company' => 'ORCA Services AG (interne Projekte)', 'currency' => 'CHF', 'customer' => [ 'id' => 1, 'name' => 'OSe', ], ], $account); self::assertNull($accounts[1]->customer); }
Notice the customer which is an associated table, whose name and id get loaded with the account, this is necessary to be able to show in the UI which customer an account belongs to.
This is what the same looks like for an add post endpoint
public function testAddPost(): void { $accountsTable = $this->getTableLocator()->get('Accounts'); $data = [ 'customer_id' => '1', 'uuid' => '1251e242-4ad6-43e7-842c-aeb392a0f862', 'name' => 'TestAccount', 'email' => 'test@account.ch', 'company' => 'Test Account AG', 'currency' => 'CHF', ]; $this->enableCsrfToken(); $this->loginAsSuperuser(); $this->post('/accounts/add', $data); $this->assertResponseCode(HttpStatuses::STATUS_FOUND); /** @var Account $account */ $account = $accountsTable->find()->orderByDesc('id')->first(); self::assertEquals('1251e242-4ad6-43e7-842c-aeb392a0f862', $account->uuid); self::assertEquals('1', $account->customer_id); }
Here you just test if the entry you added actually corresponds to the data that was input.
And this is what a delete endpoint test looks like
public function testDelete(): void { $id = 1; $accountsTable = $this->getTableLocator()->get('Accounts'); /** @var Account $account */ $account = $accountsTable->find()->where(['id' => $id])->first(); self::assertNotNull($account); $this->enableCsrfToken(); $this->loginAsSuperuser(); $this->post('/accounts/delete/' . $id); $this->assertResponseCode(HttpStatuses::STATUS_FOUND); $this->assertRedirect('/'); /** @var Account $account */ $account = $accountsTable->find()->where(['id' => $id])->first(); self::assertNull($account); }
Here you test beforehand if the entry with the selected ID exists (it should), then you call the delete endpoint and check the entry again, now it shouldnt exist.
For all endpoints you also check if it correctly redirects you (if it should do that).
You also check every endpoint as unauthenticated (unless it should be available as unauthenticated) and check if it correctly redirects to the login.
