Тестирование контроллера 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: 

Проблема

На самом деле здесь есть три проблемы:

  1. Тестовый код очень повторяющийся.
  2. Вторая "Измененная" строка в выходных данных теста пуста. Это должно быть "2013-05-08 00:00:00", как в выводе из браузер.
  3. Если бы я изменил код контроллера, добавив строку с надписью 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') код в любом месте, не нарушая тесты?

Author: Nick, 2013-05-08

1 answers

Прежде чем я отвечу на этот вопрос, я должен признать, что у меня нет опыта использования фреймворка CakePHP. Тем не менее, у меня есть достаточный опыт работы с PHPUnit в сочетании с платформой Symfony, и я сталкивался с подобными проблемами. Чтобы ответить на ваши вопросы:

  1. Смотрите мой ответ на вопрос 3

  2. Причина этого в том, что вам нужна дополнительная инструкция ...->staticExpects($this->at(5))... для покрытия 6-го вызова Auth->user(). Эти инструкции не определяют возвращаемые значения для любого вызова Auth->user() с указанным значением. Они определяют, что, например, 2-й вызов объекта аутентификации должен быть для метода user() с параметром "имя пользователя", в этом случае будет возвращено "администратор". Однако это больше не должно быть проблемой, если вы будете следовать подходу, описанному в следующем пункте.

  3. Я делаю предположение, что вы пытаетесь здесь протестировать свой контроллер независимо от компонента аутентификации (потому что, честно говоря, нет смысла проверять, что контроллер выполняет серию вызовов 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] будет работать так же хорошо.

 8
Author: redbirdo, 2014-05-23 14:42:25