Тестирование контроллера CakePHP: Издевательство над компонентом аутентификации
Ситуация
Код контроллера
<?php
App::uses('AppController', 'Controller');
class PostsController extends AppController {
public function isAuthorized() {
return true;
}
public function edit($id = null) {
$this->autoRender = false;
if (!$this->Post->exists($id)) {
throw new NotFoundException(__('Invalid post'));
}
if ($this->Post->find('first', array(
'conditions' => array(
'Post.id' => $id,
'Post.user_id' => $this->Auth->user('id')
)
))) {
echo 'Username: ' . $this->Auth->user('username') . '<br>';
echo 'Created: ' . $this->Auth->user('created') . '<br>';
echo 'Modified: ' . $this->Auth->user('modified') . '<br>';
echo 'All:';
pr($this->Auth->user());
echo 'Modified: ' . $this->Auth->user('modified') . '<br>';
} else {
echo 'Unauthorized.';
}
}
}
Вывод из браузера
Username: admin
Created: 2013-05-08 00:00:00
Modified: 2013-05-08 00:00:00
All:
Array
(
[id] => 1
[username] => admin
[created] => 2013-05-08 00:00:00
[modified] => 2013-05-08 00:00:00
)
Modified: 2013-05-08 00:00:00
Тестовый Код
<?php
App::uses('PostsController', 'Controller');
class PostsControllerTest extends ControllerTestCase {
public $fixtures = array(
'app.post',
'app.user'
);
public function testEdit() {
$this->Controller = $this->generate('Posts', array(
'components' => array(
'Auth' => array('user')
)
));
$this->Controller->Auth->staticExpects($this->at(0))->method('user')->with('id')->will($this->returnValue(1));
$this->Controller->Auth->staticExpects($this->at(1))->method('user')->with('username')->will($this->returnValue('admin'));
$this->Controller->Auth->staticExpects($this->at(2))->method('user')->with('created')->will($this->returnValue('2013-05-08 00:00:00'));
$this->Controller->Auth->staticExpects($this->at(3))->method('user')->with('modified')->will($this->returnValue('2013-05-08 00:00:00'));
$this->Controller->Auth->staticExpects($this->at(4))->method('user')->will($this->returnValue(array(
'id' => 1,
'username' => 'admin',
'created' => '2013-05-08 00:00:00',
'modified' => '2013-05-08 00:00:00'
)));
$this->testAction('/posts/edit/1', array('method' => 'get'));
}
}
Результат теста
Username: admin
Created: 2013-05-08 00:00:00
Modified: 2013-05-08 00:00:00
All:
Array
(
[id] => 1
[username] => admin
[created] => 2013-05-08 00:00:00
[modified] => 2013-05-08 00:00:00
)
Modified:
Проблема
На самом деле здесь есть три проблемы:
- Тестовый код очень повторяющийся.
- Вторая "Измененная" строка в выходных данных теста пуста. Это должно быть "2013-05-08 00:00:00", как в выводе из браузер.
- Если бы я изменил код контроллера, добавив строку с надписью
echo 'Email: ' . $this->Auth->user('email') . '<br>';
(просто для примера) междуecho
"Имя пользователя" и "Создано", тест не прошел бы с этой ошибкой:Expectation failed for method name is equal to <string:user> when invoked at sequence index 2
. Это имеет смысл, поскольку$this->at(1)
больше не соответствует действительности.
Мой вопрос
Как я могу издеваться над компонентом аутентификации таким образом, чтобы (1) не повторялся, (2) заставлял тест выводить то же самое, что и браузер, и (3) позволял мне добавлять $this->Auth->user('foo')
код в любом месте, не нарушая тесты?
1 answers
Прежде чем я отвечу на этот вопрос, я должен признать, что у меня нет опыта использования фреймворка CakePHP. Тем не менее, у меня есть достаточный опыт работы с PHPUnit в сочетании с платформой Symfony, и я сталкивался с подобными проблемами. Чтобы ответить на ваши вопросы:
Смотрите мой ответ на вопрос 3
Причина этого в том, что вам нужна дополнительная инструкция
...->staticExpects($this->at(5))...
для покрытия 6-го вызова Auth->user(). Эти инструкции не определяют возвращаемые значения для любого вызова Auth->user() с указанным значением. Они определяют, что, например, 2-й вызов объекта аутентификации должен быть для метода user() с параметром "имя пользователя", в этом случае будет возвращено "администратор". Однако это больше не должно быть проблемой, если вы будете следовать подходу, описанному в следующем пункте.-
Я делаю предположение, что вы пытаетесь здесь протестировать свой контроллер независимо от компонента аутентификации (потому что, честно говоря, нет смысла проверять, что контроллер выполняет серию вызовов getter для объекта пользователя). В этом случае макет объекта настраивается как заглушка, чтобы всегда возвращать определенный набор результатов, а не ожидать определенной серии вызовов с определенными параметрами ( См. Ввод вручную PHP на заглушках ). Это можно было бы сделать, просто заменив "$this->at(x)" на "$this->any()" в вашем коде. Однако, хотя это исключило бы необходимость добавления дополнительной строки, о которой я упоминал в пункте 2, у вас все равно будет повторение. Следуя подходу TDD к написанию тестов перед кодом, я бы предложил следующее:
public function testEdit() { $this->Controller = $this->generate('Posts', array( 'components' => array( 'Auth' => array('user') ) )); $this->Controller->Auth ->staticExpects($this->any()) ->method('user') ->will($this->returnValue(array( 'id' => 1, 'username' => 'admin', 'created' => '2013-05-08 00:00:00', 'modified' => '2013-05-08 00:00:00', 'email' => '[email protected]', ))); $this->testAction('/posts/edit/1', array('method' => 'get')); }
Это позволит вашему контроллеру обновляться, чтобы совершать столько вызовов, сколько вам нужно, чтобы получать пользовательские атрибуты в любом порядке при условии, что они уже возвращены фиктивным объектом. Ваш макет объекта может быть написан так, чтобы возвращать все пользовательские атрибуты (или, возможно, все, которые могут иметь отношение к этому контроллеру), независимо от того, извлекает ли их контроллер и как часто. (Примечание в ваш конкретный пример, если ваш макет содержит "электронную почту", оператор pr() в контроллере выведет результаты теста, отличные от результатов браузера, но я предполагаю, что вы не ожидаете, что сможете добавлять новые атрибуты в запись без необходимости обновлять свои тесты).
Написание теста таким образом означает, что ваша функция редактирования контроллера должна быть примерно такой - более тестируемая версия:
$this->autoRender = false;
if (!$this->Post->exists($id)) {
throw new NotFoundException(__('Invalid post'));
}
$user = $this->Auth->user();
if ($this->Post->find('first', array(
'conditions' => array(
'Post.id' => $id,
'Post.user_id' => Hash::get($user, 'id')
)
))) {
echo 'Username: ' . Hash::get($user, 'username') . '<br>';
echo 'Created: ' . Hash::get($user, 'created') . '<br>';
echo 'Modified: ' . Hash::get($user, 'modified') . '<br>';
echo 'All:';
pr($user);
echo 'Modified: ' . Hash::get($user, 'modified') . '<br>';
} else {
echo 'Unauthorized.';
}
Насколько я могу судить, Hash::get($запись, $ключ) - это правильный CakePHP способ извлечения атрибутов из записи, хотя с помощью простых атрибутов, которые у вас здесь есть, я полагаю, что user[$key] будет работать так же хорошо.