воскресенье, 23 сентября 2012 г.

Mockery — отличный фреймворк для тестирования с мок - объектами

Здравствуйте, читатели!

Я хочу поделиться своим опытом написания нового кода под управлением тестов. Мне не совсем ясно зачем может понадобится написание тестов для готового работающего кода, разве что вы собираетесь сильно изменить его, и готовите безопасную почву. Но рефакторинг — отдельная большая тема, а пока сосредоточимся на созидательной работе. Далее я буду говорить о тестировании, подразумевая написание нового кода, когда тесты - вперед.

В деле юнит тестирования важную роль играют мок- и стаб- объекты.

Стабы — это затычки, они позволяют подделать какую-либо функциональность, реализация которой не интересна при тестировании кода, над которым вы работаете.

Мок может протестировать взаимодействие объектов, он рапортует об ошибке, если ваш класс использовал его неожиданным образом: вызвал с неожиданными параметрами или неправильное количество раз.

Положительные эмоции вызывают лаконичные и простые тесты. Mockery — находка в этом смысле. Этот фреймворк позволяет описать работу объекта кратко. С другой стороны он дает возможность запрограммировать довольно интересные случаи.

1. Создаем стабы.

1.1

$auth = m::mock(array('allowed' => true));
Так получается объект, который при вызове allowed() вернет true. Он ничего не тестирует, это просто затычка. Его можно передать в тестируемый класс, когда вы хотите проверить что происходит, например, с пользователем, которому разрешен доступ куда-либо.

1.2

Если вам необходимо получить стаб определенного типа, нужно передать имя класса или интерфейса первым параметром:
$auth = m::mock('Auth_Interface', array('allowed' => true));

1.3 

Можно получить объект, который будет вести себя по-разному, в зависимости от параметров, с которыми он будет вызван:
$db = m::mock();
$db->shouldReceive('exec')
    ->with(containsString('SELECT'))
    ->andReturn(array('fake data'));
$db->shouldReceive('exec')
    ->with(containsString('INSERT'))
    ->andReturn(1);
Такой объект будет возвращать массив, при вызове exec() со строкой, содержащей SELECT, и единицу, если в строке было слово INSERT. Здесь 'containsString' - возвращает matcher — объект, который описывает ожидаемое поведение.

1.4 

Вот как получить объект, который при каждом следующем вызове вернет разные данные:
$db = m::mock();
$db->shouldReceive('pop')->andReturn(4, 23, 44, 88);

1.5 

Чтобы запрограммировать какую-нибудь сложную логику работы с аргументами, можно использовать анонимную функцию для создания возвращаемого значения:
$anonymizer = m::mock();
$anonymizer->shouldReceive('hideName')->andReturnUsing(
    function($user){
        $user->name = 'anonymous';
        return $user;
    }
);
Этот объект будет передавать дальше каждого переданного ему пользователя, предварительно делая его анонимным.

1.6 

Когда метод должен изменить аргументы, передаваемые по ссылке, нужно снова использовать анонимную функцию.
$legacyDb = m::mock();
$legacyDb->shouldReceive('select')
    ->with(
        m::on(function(&$list) {
            $list = array(array('id' => 1));
            return true;
        }), 
        anything()
    );
При вызове $legacyDb->select($list, 'SELECT id') такого объекта в первый параметр будет записан массив.

2. Создаем моки. 

Описанные выше стабы не тестируют ничего, они просто подменяют собой настоящие объекты. В принципе без них вполне можно обойтись, создавая простые объекты, выполняющие нужные действия. Чтобы что-то протестировать, необходимо к стабу добавить какие-либо ожидания — превратить его в мок.

2.1

$dbAdapter = m::mock();
$dbAdapter->shouldReceive('delete')->once();
Вот я создал объект $dbAdapter и ожидаю что его метод delete() будет вызван единожды. Если до конца выполнения теста этого не произойдет, или же метод будет вызван чаще, тест развалится.

2.2

Чтобы протестировать параметры вызова, используется такой прием:
$crypt = m::mock();
$crypt->shouldReceive('encrypt')
    ->with(anything(), self::companyPublicKey())
    ->once();
Так можно проверить, что для шифрования какого-либо сообщения будет использован определенный ключ, который вы поместили в возврат функции companyPublicKey().

2.3

Свободу в проверке аргументов вызова даёт использование анонимных функций. Вот так:
$importer = m::mock();
$importer->shouldReceive('import')
    ->with(m::on(
        function($company) {
            return $company->source == 'SOAP';
        }))
    ->atLeastOnce();
Ожидается, что в этот объект на обработку будет передана хотя бы одна компания, с источником 'SOAP'.

2.4

Если тестируемый объект будет реализовывать так называемый 'fluent interface', позволяя вызывать свои методы цепочкой, необходимо создавать методы, возвращающие сам объект. Это делается так:
$style = m::mock();
$style->shouldReceive('color')->once()->andReturn(m::self());
Такой объект можно смело передавать туда, где используется цепочка вызовов, он её не поломает. Я имею в виду так:
$style->color('white')->width(5); 

2.5 

Есть способ протестировать порядок вызовов. Например, логику, когда ваш код должен будет вначале вставить данные в базу, а потом — получить Id, можно тестировать так:
$db = m::mock();
$db->shouldReceive('insert')->once()->ordered(); 
$db->shouldReceive('lastInsertedId')->once()->ordered(); 
Если порядок вызовов будет нарушен вы об этом узнаете.

3. Заключение 

Многие другие возможности Mockery я не рассматривал, приглашаю изучать документацию здесь: https://github.com/padraic/mockery

4 комментария:

  1. Для меня очень полезным оказался метод m::mock()->shouldIgnoreMissing() (разрешать и игнорировать всё, что ты явно не стабишь или мокаешь). И даже странно, что это -- не поведение по умолчанию.

    ОтветитьУдалить
    Ответы
    1. Классно делать это одной строкой:

      m::mock('Mock Name', function($mock){$mock->shouldIgnoreMissing();});

      Удалить
  2. Неплохо бы в двух словах о том, откуда берется m :)
    use Mockery as m;

    ОтветитьУдалить
  3. а я сегодня решил замокать объект, чтобы передать его в метод другого класса. и на столько мне стало лень с этим мокери разбираться, что я понял что там и не нужен этот объект ,)
    максим назвал этот случай "лень дривинг девелопмент"

    ОтветитьУдалить