четверг, 28 июля 2011 г.

Использование моков в PHPUnit

Это спасенный из кеша яндекса топик в блоге.


Короткое введение



Вначале хочу уточнить, что такое моки и зачем они нужны.

Моки это объекты-заглушки, которые заменяют реальные объекты, эмулируют их поведение. Моки используются в модульном тестировании, для:
  • изоляции тестируемого объекта;
  • тестирования делегирования.


Рассмотрим все на примере. Допустим у нас есть класс Foo, который мы будем тестировать:
class Foo
{
    public function bar()
    {

    }
}

В методе bar() объект класса Foo делегирует (перенаправляет) операцию другому объекту:
class Foo
{
    protected $_obj;

    public function bar()
    {
        $this->_obj->someMethod();
    }

    public function setSomeObject(SomeClass $obj)
    {
        $this->_obj = $obj;
        return $this;
    }
}

Налицо зависимость класса Foo от объекта класса SomeClass. Если поведение метода SomeClass::someMethod() мы не в состоянии контроллировать или это слишком сложно, имеет смысл заменить объкт $obj фиктивным мок-объектом. Такая ситуация возможна, если, например, метод SomeClass::someMethod() ведет работу с БД.

Сделаю оговорку, что использование мок-объекта вместо реального оправдано не всегда.

Чтобы успешно использовать моки и грамотно спроектировать класс необходимо понимать принципы Dependency Injection, которые отлично описаны здесь.

Общая суть применения принципов инверсии зависимостей сводится к следующему - взаимодействия внутри системы начинают строиться на основе интерфейсов, а не классов (реализации). Классы как бы отказываются от знаний, кому именно они делегируют обязанности. Для них становится важным только то, чтобы делегируемые объекты умели делать то что должны, кто они именно такие при этом - неважно. Обычно применение принципов Dependency Injection положительно сказывается на проектах: повышается гибкость кода, повышается повторное использование, упрощается тестирование и т.д. Самое главное здесь - не переборщить, чрезмерное абстрагирование может сильно повредить проекту, особенно если это абстрагирование было реализовано только для облегчения тестирования.

Учимся пользоваться моками в  PHPUnit 


Получить
 Mock-объект  в  PHPUnit  можно с помощью метода  PHPUnit _Framework_TestCase::getMock(), который имеет следующую сигнатуру:
protected function getMock($className, array $methods = array(), 
array $arguments = array(), $mockClassName = '', 
$callOriginalConstructor = TRUE, 
                           $callOriginalClone = TRUE, $callAutoload = TRUE)

Этот метод возвращает мок-объект, который впоследствии можно настроить.

Рассмотрю параметры этого метода:
- $className (string) - имя класса, на базе которого будет создан мок-объект.
- $methods (array) - по умолчанию мок-объект будет содержать все методы базового класса (класса на основе которого создается мок), причем это будут методы-заглушки, возвращающие просто значение null. Если нужно сделать так, чтобы заменялись только определенные методы, а остальные оставались реальными, или нужны новые методы, их имена надо передать в этом массиве.
- $arguments (array) - массив параметров, который будет передан конструктору мока. Каждый аргумент описывается массивом в формате: array('имя переменной' => 'значение'). Таким образом, при необходимости, в качестве этого параметра надо передавать двумерный массив.
- $mockClassName (string) - имя класса, которое получит мок. Если этот параметр не указан, мок получит имя в формате:
' Mock _' . 'Имя_класса' . '_произвольная_строка'

Например, если $className равен 'Foo', а параметр $mockClassName опущен, то сгенерированный мок будет иметь имя Foo_e4a66bd9.
- $callOriginalConstructor (boolean) - если равен false, то не будет вызван конструктор оригинального объекта.
- $callOriginalClone (boolean) - если равен false, то не будет вызван метод __clone() оригинального объекта.
- $callAutoload (boolean) - если равен false, то при создании мока не будет использована автозагрузка с помощью __autoload().

Обязателен только первый параметр. Пример получения мока с двумя методами getFoo() и setFoo():
$mock  = $this->getMock('Myak', array('getFoo', 'setFoo'));

После создания мока необходимо произвести настройку его методов.

Ожидаемое количество вызовов метода настраивается с помощью метода expects(), которому передается в качестве аргумента один из фабричных методов (под $this здесь подразумевается объект класса  PHPUnit _Framework_TestCase), возвращающих объект-matcher:
  • $this->any() - возвращаемый этим методом, матчер контроллирует, что метод будет вызван 0 или более раз;
  • $this->never() - возвращаемый этим методом, матчер контроллирует, что метод не должен быть вызван ни одного раза;
  • $this->atLeastOnce() - возвращаемый этим методом, матчер контроллирует, что метод должен быть вызван по крайней мере один раз;
  • $this->once() - возвращаемый этим методом, матчер контроллирует, что метод должен быть вызван только один раз;
  • $this->exactly(int $count) - возвращаемый этим методом, матчер контроллирует, что метод должен быть вызван точно указанное количество раз, которое задается целочисленным параметром $count.
  • $this->at(int $index) - возвращаемый этим методом, матчер контроллирует, что метод будет вызван именно таким по счету, который задан целочисленным параметром $index.

Пример настройки количества вызовов:
$mock->expects($this->once());
этой строкой я говорю, что ожидается вызов настраивомого метода только один раз.

После настройки количества вызовов метода, необходимо указать какой-конкретно метод мы настраиваем. Это делается так:
$mock->expects($this->once())->method('someMethod');
someMethod естественно нужно заменить названием настраиваемого метода.

Параметры, с которыми будет вызван настраиваемый метод, задается с помощью метода with(). Этому методу можно передавать через запятую либо необходимые переменные, либо ограничения (constraints). Полный список ограничений и их описание можно найти здесь. Ограничения используются для проверки параметров.

Если параметры не нужно проверять, то можно просто использовать метод withAnyParameters().

Пример настройки параметров:
$ mock ->expects($this->once())
     ->method('someMethod')
     ->with($this->equalTo('value'));
этой строкой, я говорю, что ожидается его вызов метода 'someMethod' с параметром 'value'.

Если необходимо, чтобы метод возвращал определенное значение, это можно настроить с помощью метода will(). Также этот метод можно использовать для того, чтобы настраиваемый метод бросал исключения. В качестве параметра методу will() передается один из следующих фабричных методов класса  PHPUnit _Framework_TestCase:
- returnValue($value) - метод будет возвращать значение $value.
- returnArgument($argumentIndex) - метод будет возвращать неизменный аргумент, которые мы передали до этого (когда настраивали параметры).
- returnCallback($callback) - метод будет возвращать callback-функцию. Функция $callback должна быть где-то определена. Это может потребоваться, если настраиваемый метод должен возвращать вычисляемое, а не фиксированное значение.
- throwException(Exception $exception) - метод будет бросать исключение $exception.
- onConsecutiveCalls() - метод будет последовательно возвращать переданные аргументы, например, если указать:
$ mock ->expects($this->once())
     ->method('someMethod')
     ->with($this->equalTo('value'))
     ->will($this->onConsecutiveCalls('value1', new Exception('My exception'), 
            'value2'));
то настраиваемый метод вначале вернет значение 'value1', затем бросит исключение, а при третьем вызове вернет значение 'value2'.

Это все, что можно сделать с моками. В конце статьи, приведу парочку примеров.

Вот пример использования моков, взятый мной из мануала к  PHPUnit :
public function testUpdateIsCalledOnce()
{
    // Create a  Mock  Object for the Observer class
    // mocking only the update() method.
    $observer = $this->getMock('Observer', array('update'));

    // Set up the expectation for the update() method
    // to be called only once and with the string 'something'
    // as its parameter.
    $observer->expects($this->once())
             ->method('update')
             ->with($this->equalTo('something'));

    // Create a Subject object and attach the mocked
    // Observer object to it.
    $subject = new Subject;
    $subject->attach($observer);

    // Call the doSomething() method on the $subject object
    // which we expect to call the mocked Observer object's
    // update() method with the string 'something'.
    $subject->doSomething();
}


А вот более сложный пример, взятый мной из тестов к Myak-CMS:
public function testLoadModel()
{
    $modelLoader = $this->_controller->getHelper('modelLoader');
    $mockLoader = $this->getMock(get_class($modelLoader),
array('load'), 
array(), 'ModelLoader');
    $mockLoader->expects($this->once())
               ->method('load')
               ->with($this->equalTo('TestModel'), $this->equalTo('test_module'),
$this->equalTo(array('opt1' => 'val1')));
    Zend_Controller_Action_HelperBroker::resetHelpers();
    Zend_Controller_Action_HelperBroker::addHelper($mockLoader);
    $this->_controller->loadModel('TestModel', 'test_module', 
    array('opt1' => 'val1'));
}

Принимаются замечания и дополнения к этой статье.

Ссылки по теме и используемые источники


Слайды по теме
Скачать  PHPUnit 
 PHPUnit  Manual
Мок-объекты в модульном тестировании. Как избежать проблем
Инверсия зависимостей при проектировании Объектно-Ориентированных систем
 Mocks  Aren't Stubs

Комментариев нет:

Отправить комментарий