Mockery — это расширение PHP, которое предлагает превосходные возможности для насмешек, особенно по сравнению с PHPUnit . Несмотря на то, что среда пересмотра PHPUnit является мощной, Mockery предлагает более естественный язык с набором совпадений, подобным Hamcrest. В этой статье я буду сравнивать две фреймворки и расскажу о лучших возможностях Mockery.
Mockery предлагает набор связанных с насмешками совпадений, которые очень похожи на словарь Hamcrest, предлагая очень естественный способ выразить насмешливые ожидания. Насмешка не переопределяет и не конфликтует со встроенными насмешливыми функциями PHPUnit; фактически вы можете использовать их одновременно (и даже в одном и том же методе тестирования).
Установка издевательства
Есть несколько способов установить Mockery; Вот самые распространенные методы.
Используйте Composer
Создайте файл с именем composer.json в корневой папке вашего проекта и добавьте в него следующий код:
|
1
2
3
4
5
|
{
«require»: {
«Mockery/Mockery»: «>=0.7.2»
}
}
|
Затем просто установите Composer в корневую папку вашего проекта с помощью следующей команды:
|
1
|
curl -s http://getcomposer.org/installer |
|
Наконец, установите все необходимые зависимости (включая Mockery) с помощью этой команды:
|
1
|
php composer.phar install
|
Установив все, давайте удостоверимся, что наша установка Mockery работает. Для простоты я предполагаю, что у вас есть папка с именем Test в корневом каталоге вашего проекта. Все примеры в этом руководстве будут находиться в этой папке. Вот код, который я использовал, чтобы убедиться, что Mockery работает с моим проектом:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
//Filename: JustToCheckMockeryTest.php
require_once ‘../vendor/autoload.php’;
class JustToCheckMockeryTest extends PHPUnit_Framework_TestCase {
protected function tearDown() {
\Mockery::close();
}
function testMockeryWorks() {
$mock = \Mockery::mock(‘AClassToBeMocked’);
$mock->shouldReceive(‘someMethod’)->once();
$workerObject = new AClassToWorkWith;
$workerObject->doSomethingWit($mock);
}
}
class AClassToBeMocked {}
class AClassToWorkWith {
function doSomethingWit($anotherClass) {
return $anotherClass->someMethod();
}
}
|
Пользователи Linux: используйте пакеты вашего дистрибутива
Некоторые дистрибутивы Linux упрощают установку Mockery, но лишь немногие предоставляют пакет Mockery для своей системы. Следующий список — единственные дистрибутивы, о которых я знаю:
- Sabayon :
equo install Mockery - Fedora / RHE : ням
yum install Mockery
Используйте грушу
Поклонники PEAR могут установить Mockery, выполнив следующие команды:
|
1
2
3
|
sudo pear channel-discover pear.survivethedeepend.com
sudo pear channel-discover hamcrest.googlecode.com/svn/pear
sudo pear install —alldeps deepend/Mockery
|
Установка из источника
Установка с GitHub для настоящих гиков! Вы всегда можете получить последнюю версию Mockery через репозиторий GitHub.
|
1
2
3
4
|
git clone git://github.com/padraic/Mockery.git
cd Mockery
sudo pear channel-discover hamcrest.googlecode.com/svn/pear
sudo pear install —alldeps package.xml
|
Создание нашего первого макетируемого объекта
Давайте посмеемся над некоторыми объектами, прежде чем мы определим какие-либо ожидания. Следующий код изменит предыдущий пример, включив в него примеры PHPUnit и Mockery:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
//Filename: MockeryABetterWayOfMockingTest.php
require_once ‘../vendor/autoload.php’;
class MockeryVersusPHPUnitGetMockTest extends PHPUnit_Framework_TestCase {
function testCreateAMockedObject() {
// With PHPUnit
$phpunitMock = $this->getMock(‘AClassToBeMocked’);
// With Mockery
$mockeryMock = \Mockery::mock(‘AClassToBeMocked’);
}
}
class AClassToBeMocked {
}
|
Насмешка позволяет вам определять издевательства для классов, которые не существуют.
Первая строка гарантирует, что у нас есть доступ к Mockery. Затем мы создаем тестовый класс с именем MockeryVersusPHPUnitGetMockTest , у которого есть метод testCreateAMockedObject() . AClassToBeMocked класс AClassToBeMocked в настоящее время полностью пуст; фактически, вы можете полностью удалить класс, не вызывая сбой теста.
Тестовый метод testCreateAMockedObject() определяет два объекта. Первый — макет PHPUnit, а второй — с помощью Mockery. Синтаксис издевательства:
|
1
|
$mockedObject = \Mockery::mock(‘SomeClassToBeMocked’);
|
Назначьте простые ожидания
Насмешки обычно используются для проверки поведения объекта (прежде всего его методов) путем указания так называемых ожиданий . Давайте настроим несколько простых ожиданий.
Ожидайте, что метод будет вызван
Вероятно, наиболее распространенное ожидание — это ожидание вызова конкретного метода. Большинство фальшивых фреймворков позволяют вам указать количество вызовов, которые вы ожидаете получить от метода. Начнем с простого ожидания одного звонка:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
//Filename: MockeryABetterWayOfMockingTest.php
require_once ‘../vendor/autoload.php’;
class MockeryVersusPHPUnitGetMockTest extends PHPUnit_Framework_TestCase {
protected function tearDown() {
\Mockery::close();
}
function testExpectOnce() {
$someObject = new SomeClass();
// With PHPUnit
$phpunitMock = $this->getMock(‘AClassToBeMocked’);
$phpunitMock->expects($this->once())->method(‘someMethod’);
// Exercise for PHPUnit
$someObject->doSomething($phpunitMock);
// With Mockery
$mockeryMock = \Mockery::mock(‘AnInexistentClass’);
$mockeryMock->shouldReceive(‘someMethod’)->once();
// Exercise for Mockery
$someObject->doSomething($mockeryMock);
}
}
class AClassToBeMocked {
function someMethod() {}
}
class SomeClass {
function doSomething($anotherObject) {
$anotherObject->someMethod();
}
}
|
Этот код настраивает ожидание для PHPUnit и Mockery. Начнем с первого.
Некоторые дистрибутивы Linux упрощают установку Mockery.
Мы используем метод expects() чтобы определить ожидание для вызова someMethod() один раз. Но для того, чтобы PHPUnit работал правильно, мы должны определить класс с именем AClassToBeMocked , и он должен иметь метод someMethod() .
Это проблема. Если вы издеваетесь над множеством объектов и разрабатываете с использованием принципов TDD для нисходящего проекта, вы не захотите создавать все классы и методы перед тестом. Ваш тест должен провалиться по правильной причине, что ожидаемый метод не был вызван, вместо критической ошибки PHP, не имеющей отношения к реальной реализации. Пойдите и попробуйте удалить определение someMethod() из AClassToBeMocked и посмотрите, что произойдет.
С другой стороны, насмешка позволяет вам определять насмешки для несуществующих классов.
Обратите внимание, что в приведенном выше примере создается макет для AnInexistentClass , который, как следует из его названия, не существует (равно как и метод someMethod() ).
В конце приведенного выше примера мы определяем класс SomeClass для реализации нашего кода. Мы инициализируем объект, называемый $someObject в первой строке тестового метода и эффективно используем код после определения наших ожиданий.
Обратите внимание: Mockery оценивает ожидания по методу close() . По этой причине у вас всегда должен быть метод tearDown() который вызывает \Mockery::close() . В противном случае издевательство дает ложные срабатывания.
Ожидайте больше, чем один звонок
Как я уже отмечал ранее, большинство фальшивых фреймворков имеют возможность указывать ожидания для нескольких вызовов методов. Для этой цели PHPUnit использует конструкцию $this->exactly() . Следующий код определяет ожидания для вызова метода несколько раз:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
function testExpectMultiple() {
$someObject = new SomeClass();
// With PHPUnit 2 times
$phpunitMock = $this->getMock(‘AClassToBeMocked’);
$phpunitMock->expects($this->exactly(2))->method(‘someMethod’);
// Exercise for PHPUnit
$someObject->doSomething($phpunitMock);
$someObject->doSomething($phpunitMock);
// With Mockery 2 times
$mockeryMock = \Mockery::mock(‘AnInexistentClass’);
$mockeryMock->shouldReceive(‘someMethod’)->twice();
// Exercise for Mockery
$someObject->doSomething($mockeryMock);
$someObject->doSomething($mockeryMock);
// With Mockery 3 times
$mockeryMock = \Mockery::mock(‘AnInexistentClass’);
$mockeryMock->shouldReceive(‘someMethod’)->times(3);
// Exercise for Mockery
$someObject->doSomething($mockeryMock);
$someObject->doSomething($mockeryMock);
$someObject->doSomething($mockeryMock);
}
|
Издевательство предоставляет два разных метода, чтобы лучше соответствовать вашим потребностям. Первый метод twice() ожидает два вызова метода. Другой метод — times() , который позволяет вам указать сумму. Подход издевательства гораздо чище и проще для чтения.
Возвращаемые значения
Другим распространенным применением mocks является проверка возвращаемого значения метода. Естественно, и PHPUnit, и Mockery имеют средства для проверки возвращаемых значений. Еще раз, давайте начнем с чего-то простого.
Простые возвращаемые значения
Следующий код содержит код PHPUnit и Mockery. Я также обновил SomeClass чтобы обеспечить тестируемое возвращаемое значение.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
class MockeryVersusPHPUnitGetMockTest extends PHPUnit_Framework_TestCase {
protected function tearDown() {
\Mockery::close();
}
// […] //
function testSimpleReturnValue() {
$someObject = new SomeClass();
$someValue = ‘some value’;
// With PHPUnit
$phpunitMock = $this->getMock(‘AClassToBeMocked’);
$phpunitMock->expects($this->once())->method(‘someMethod’)->will($this->returnValue($someValue));
// Expect the returned value
$this->assertEquals($someValue, $someObject->doSomething($phpunitMock));
// With Mockery
$mockeryMock = \Mockery::mock(‘AnInexistentClass’);
$mockeryMock->shouldReceive(‘someMethod’)->once()->andReturn($someValue);
// Expect the returned value
$this->assertEquals($someValue, $someObject->doSomething($mockeryMock));
}
}
class AClassToBeMocked {
function someMethod() {
}
}
class SomeClass {
function doSomething($anotherObject) {
return $anotherObject->someMethod();
}
}
|
API PHPUnit и Mockery просты и просты в использовании, но я по-прежнему считаю, что Mockery чище и удобочитаемее.
Возврат разных значений
Частые юнит-тестеры могут свидетельствовать о сложностях с методами, которые возвращают разные значения. К сожалению, метод $this->at($index) PHPUnit — единственный способ вернуть разные значения из одного и того же метода. Следующий код демонстрирует метод at() :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
function testDemonstratePHPUnitCallIndexing() {
$someObject = new SomeClass();
$firstValue = ‘first value’;
$secondValue = ‘second value’;
// With PHPUnit
$phpunitMock = $this->getMock(‘AClassToBeMocked’);
$phpunitMock->expects($this->at(0))->method(‘someMethod’)->will($this->returnValue($firstValue));
$phpunitMock->expects($this->at(1))->method(‘someMethod’)->will($this->returnValue($secondValue));
// Expect the returned value
$this->assertEquals($firstValue, $someObject->doSomething($phpunitMock));
$this->assertEquals($secondValue, $someObject->doSomething($phpunitMock));
}
|
Этот код определяет два отдельных ожидания и делает два разных вызова someMethod() ; Итак, этот тест проходит. Но давайте введем поворот и добавим двойной вызов в тестируемом классе:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// […] //
function testDemonstratePHPUnitCallIndexingOnTheSameClass() {
$someObject = new SomeClass();
$firstValue = ‘first value’;
$secondValue = ‘second value’;
// With PHPUnit
$phpunitMock = $this->getMock(‘AClassToBeMocked’);
$phpunitMock->expects($this->at(0))->method(‘someMethod’)->will($this->returnValue($firstValue));
$phpunitMock->expects($this->at(1))->method(‘someMethod’)->will($this->returnValue($secondValue));
// Expect the returned value
$this->assertEquals(‘first value second value’, $someObject->concatenate($phpunitMock));
}
class SomeClass {
function doSomething($anotherObject) {
return $anotherObject->someMethod();
}
function concatenate($anotherObject) {
return $anotherObject->someMethod() .
}
}
|
Тест все еще проходит. PHPUnit ожидает два вызова someMethod() которые происходят внутри тестируемого класса при выполнении конкатенации с помощью метода concatenate() . Первый вызов возвращает первое значение, а второй вызов возвращает второе значение. Но здесь есть одна загвоздка: что произойдет, если вы удвоите утверждение? Вот код:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
function testDemonstratePHPUnitCallIndexingOnTheSameClass() {
$someObject = new SomeClass();
$firstValue = ‘first value’;
$secondValue = ‘second value’;
// With PHPUnit
$phpunitMock = $this->getMock(‘AClassToBeMocked’);
$phpunitMock->expects($this->at(0))->method(‘someMethod’)->will($this->returnValue($firstValue));
$phpunitMock->expects($this->at(1))->method(‘someMethod’)->will($this->returnValue($secondValue));
// Expect the returned value
$this->assertEquals(‘first value second value’, $someObject->concatenate($phpunitMock));
$this->assertEquals(‘first value second value’, $someObject->concatenate($phpunitMock));
}
|
Возвращает следующую ошибку:
|
1
2
3
4
5
6
|
Failed asserting that two strings are equal.
— Expected
+++ Actual
@@ @@
-‘first value second value’
+’ ‘
|
PHPUnit продолжает считать между различными вызовами concatenate() . К тому времени, когда происходит второй вызов в последнем утверждении, $index принимает значения 2 и 3 . Вы можете пройти тест, изменив свои ожидания, чтобы рассмотреть два новых шага, например:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
function testDemonstratePHPUnitCallIndexingOnTheSameClass() {
$someObject = new SomeClass();
$firstValue = ‘first value’;
$secondValue = ‘second value’;
// With PHPUnit
$phpunitMock = $this->getMock(‘AClassToBeMocked’);
$phpunitMock->expects($this->at(0))->method(‘someMethod’)->will($this->returnValue($firstValue));
$phpunitMock->expects($this->at(1))->method(‘someMethod’)->will($this->returnValue($secondValue));
$phpunitMock->expects($this->at(2))->method(‘someMethod’)->will($this->returnValue($firstValue));
$phpunitMock->expects($this->at(3))->method(‘someMethod’)->will($this->returnValue($secondValue));
// Expect the returned value
$this->assertEquals(‘first value second value’, $someObject->concatenate($phpunitMock));
$this->assertEquals(‘first value second value’, $someObject->concatenate($phpunitMock));
}
|
Вы, вероятно, можете жить с этим кодом, но Mockery делает этот сценарий тривиальным. Не веришь мне? Взглянем:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
|
function testMultipleReturnValuesWithMockery() {
$someObject = new SomeClass();
$firstValue = ‘first value’;
$secondValue = ‘second value’;
// With Mockery
$mockeryMock = \Mockery::mock(‘AnInexistentClass’);
$mockeryMock->shouldReceive(‘someMethod’)->andReturn($firstValue, $secondValue, $firstValue, $secondValue);
// Expect the returned value
$this->assertEquals(‘first value second value’, $someObject->concatenate($mockeryMock));
$this->assertEquals(‘first value second value’, $someObject->concatenate($mockeryMock));
}
|
Как и PHPUnit, Mockery использует подсчет индексов, но нам не нужно беспокоиться об индексах. Вместо этого мы просто перечисляем все ожидаемые значения, и Mockery возвращает их по порядку.
Кроме того, PHPUnit возвращает NULL для неопределенных индексов, но Mockery всегда возвращает последнее указанное значение. Это приятное прикосновение.
Попробуйте несколько методов с индексированием
Давайте введем второй метод в наш код, метод concatWithMinus() :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
class SomeClass {
function doSomething($anotherObject) {
return $anotherObject->someMethod();
}
function concatenate($anotherObject) {
return $anotherObject->someMethod() .
}
function concatWithMinus($anotherObject) {
return $anotherObject->anotherMethod() .
}
}
|
Этот метод ведет себя аналогично concatenate() , но он объединяет строковые значения с » - «, а не с одним пробелом. Поскольку эти два метода выполняют сходные задачи, имеет смысл тестировать их внутри одного и того же метода тестирования, чтобы избежать дублирования тестирования.
Как показано в приведенном выше коде, вторая функция использует другой anotherMethod() метод с именем anotherMethod() . Я сделал это изменение, чтобы заставить нас использовать оба метода в наших тестах. Наш насмешливый класс теперь выглядит так:
|
01
02
03
04
05
06
07
08
09
10
11
|
class AClassToBeMocked {
function someMethod() {
}
function anotherMethod() {
}
}
|
Тестирование с помощью PHPUnit может выглядеть следующим образом:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
function testPHPUnitIndexingOnMultipleMethods() {
$someObject = new SomeClass();
$firstValue = ‘first value’;
$secondValue = ‘second value’;
// With PHPUnit
$phpunitMock = $this->getMock(‘AClassToBeMocked’);
// First and second call on the semeMethod:
$phpunitMock->expects($this->at(0))->method(‘someMethod’)->will($this->returnValue($firstValue));
$phpunitMock->expects($this->at(1))->method(‘someMethod’)->will($this->returnValue($secondValue));
// Expect the returned value
$this->assertEquals(‘first value second value’, $someObject->concatenate($phpunitMock));
// First and second call on the anotherMethod:
$phpunitMock->expects($this->at(0))->method(‘anotherMethod’)->will($this->returnValue($firstValue));
$phpunitMock->expects($this->at(1))->method(‘anotherMethod’)->will($this->returnValue($secondValue));
// Expect the returned value
$this->assertEquals(‘first value — second value’, $someObject->concatWithMinus($phpunitMock));
}
|
Логика здравая. Определите два разных ожидания для каждого метода и укажите возвращаемое значение. Это работает только с PHPUnit 3.6 или новее.
Обратите внимание: в PHPunit 3.5 и более ранних версиях была ошибка, из-за которой индекс для каждого метода не сбрасывался, что приводило к неожиданным возвращаемым значениям для ложных методов.
Давайте посмотрим на тот же сценарий с издевательством. Еще раз, мы получаем намного более чистый код. Посмотреть на себя:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
function testMultipleReturnValuesForDifferentFunctionsWithMockery() {
$someObject = new SomeClass();
$firstValue = ‘first value’;
$secondValue = ‘second value’;
// With Mockery
$mockeryMock = \Mockery::mock(‘AnInexistentClass’);
$mockeryMock->shouldReceive(‘someMethod’)->andReturn($firstValue, $secondValue);
$mockeryMock->shouldReceive(‘anotherMethod’)->andReturn($firstValue, $secondValue);
// Expect the returned value
$this->assertEquals(‘first value second value’, $someObject->concatenate($mockeryMock));
$this->assertEquals(‘first value — second value’, $someObject->concatWithMinus($mockeryMock));
}
|
Возвращаемые значения на основе данного параметра
Честно говоря, это то, что PHPUnit просто не может сделать. На момент написания этой статьи PHPUnit не разрешал вам возвращать разные значения из одной и той же функции на основе параметра функции. Поэтому следующий тест не пройден:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
// […] //
function testPHUnitCandDecideByParameter() {
$someObject = new SomeClass();
// With PHPUnit
$phpunitMock = $this->getMock(‘AClassToBeMocked’);
$phpunitMock->expects($this->any())->method(‘getNumber’)->with(2)->will($this->returnValue(2));
$phpunitMock->expects($this->any())->method(‘getNumber’)->with(3)->will($this->returnValue(3));
$this->assertEquals(4, $someObject->doubleNumber($phpunitMock, 2));
$this->assertEquals(6, $someObject->doubleNumber($phpunitMock, 3));
}
class AClassToBeMocked {
// […] //
function getNumber($number) {
return $number;
}
}
class SomeClass {
// […] //
function doubleNumber($anotherObject, $number) {
return $anotherObject->getNumber($number) * 2;
}
}
|
Пожалуйста, игнорируйте тот факт, что в этом примере нет логики; это потерпит неудачу, даже если бы оно было. Этот код, однако, помогает проиллюстрировать идею.
Этот тест не пройден, потому что PHPUnit не может различить два ожидания в тесте. Второе ожидание, ожидающее параметр 3 , просто переопределяет первый ожидающий параметр 2 . Если вы попытаетесь запустить этот тест, вы получите следующую ошибку:
|
1
2
3
|
Expectation failed for method name is equal to <string:getNumber> when invoked zero or more times
Parameter 0 for invocation AClassToBeMocked::getNumber(2) does not match expected value.
Failed asserting that 2 matches expected 3.
|
Это можно сделать с помощью насмешек, и приведенный ниже код работает точно так, как вы ожидаете. Метод возвращает разные значения в зависимости от предоставленных параметров:
|
01
02
03
04
05
06
07
08
09
10
11
|
function testMockeryReturningDifferentValuesBasedOnParameter() {
$someObject = new SomeClass();
// Mockery
$mockeryMock = \Mockery::mock(‘AnInexistentClass’);
$mockeryMock->shouldReceive(‘getNumber’)->with(2)->andReturn(2);
$mockeryMock->shouldReceive(‘getNumber’)->with(3)->andReturn(3);
$this->assertEquals(4, $someObject->doubleNumber($mockeryMock, 2));
$this->assertEquals(6, $someObject->doubleNumber($mockeryMock, 3));
}
|
Частичные издевательства
Иногда вы хотите смоделировать только определенные методы для вашего объекта (в отличие от насмешки над целым объектом). Следующий класс Calculator уже существует; мы хотим издеваться только над определенными методами:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
class Calculator {
function add($firstNo, $secondNo) {
return $firstNo + $secondNo;
}
function subtract($firstNo, $secondNo) {
return $firstNo — $secondNo;
}
function multiply($value, $multiplier) {
$newValue = 0;
for($i=0;$i<$multiplier;$i++)
$newValue = $this->add($newValue, $value);
return $newValue;
}
}
|
Этот класс Calculator имеет три метода: add() , subtract() и multiply() . Multiply использует цикл для выполнения умножения, вызывая add() определенное количество раз (например, 2 x 3 — это действительно 2 + 2 + 2 ).
Давайте предположим, что мы хотим протестировать multiply() в полной изоляции; Итак, мы будем издеваться над add() и проверим конкретное поведение в multiply() . Вот несколько возможных тестов:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
function testPartialMocking() {
$value = 3;
$multiplier = 2;
$result = 6;
// PHPUnit
$phpMock = $this->getMock(‘Calculator’, array(‘add’));
$phpMock->expects($this->exactly(2))->method(‘add’)->will($this->returnValue($result));
$this->assertEquals($result, $phpMock->multiply($value,$multiplier));
// Mockery
$mockeryMock = \Mockery::mock(new Calculator);
$mockeryMock->shouldReceive(‘add’)->andReturn($result);
$this->assertEquals($result, $mockeryMock->multiply($value,$multiplier));
// Mockery extended test checking parameters
$mockeryMock2 = \Mockery::mock(new Calculator);
$mockeryMock2->shouldReceive(‘add’)->with(0,3)->andReturn(3);
$mockeryMock2->shouldReceive(‘add’)->with(3,3)->andReturn(6);
$this->assertEquals($result, $mockeryMock2->multiply($value,$multiplier));
}
|
Издевательство предлагает … очень естественный способ выразить насмешливые ожидания.
Первый тест PHPUnit является анемичным; он просто проверяет, что метод add() вызывается дважды, и возвращает окончательное значение при каждом вызове. Это делает работу, но это также немного сложно. PHPUnit заставляет вас передавать список методов, которые вы хотите $this->getMock() как второй параметр, в $this->getMock() . В противном случае PHPUnit будет макетировать все методы, каждый из которых по умолчанию возвращает NULL . Этот список должен храниться в соответствии с ожиданиями, которые вы определяете для вашего объекта.
Например, если я добавлю второе ожидание в $phpMock substract() , PHPUnit проигнорирует его и вызовет оригинальный метод substract() . То есть, если я явно не $this->getmock() имя метода ( $this->getmock() операторе $this->getmock() .
Конечно, Mockery отличается тем, что позволяет вам предоставить реальный объект для \Mockery::mock() , и он автоматически создает частичное макетирование. Это достигается путем внедрения прокси-подобного решения для насмешек. Все ожидания, которые вы определяете, используются, но Mockery возвращается к исходному методу, если вы не укажете ожидание для этого метода.
Обратите внимание: подход Mockery очень прост, но внутренние вызовы методов не проходят через макет объекта.
Этот пример вводит в заблуждение, но он показывает, как не использовать частичные макеты Mockery. Да, Mockery создает частичное макетирование, если вы передаете реальный объект, но он только макетирует только внешние вызовы . Например, на основе предыдущего кода метод multiply() вызывает реальный метод add() . Продолжайте и попробуйте изменить последнее ожидание с ...->andReturn(6) на ...->andReturn(7) . Тест, очевидно, должен провалиться, но это не так, потому что реальный add() выполняется вместо метода mocked add() .
Но мы можем обойти эту проблему, создавая подобные насмешки:
|
1
2
3
4
|
//Instead of
$mockeryMock = \Mockery::mock(new Calculator);
// Create the mock like this
$mockeryMock = \Mockery::mock(‘Calculator[add]’);
|
Хотя синтаксически отличается, концепция похожа на подход PHPUnit: вы должны перечислить проверенные методы в двух местах. Но для любого другого теста вы можете просто пропустить реальный объект, что намного проще — особенно при работе с параметрами конструктора.
Работа с параметрами конструктора
Давайте добавим конструктор с двумя параметрами в класс Calculator . Пересмотренный код:
|
1
2
3
4
5
6
7
8
9
|
class Calculator {
public $myNumbers = array();
function __construct($firstNo, $secondNo) {
$this->myNumbers[]=$firstNo;
$this->myNumbers[]=$secondNo;
}
// […] //
}
|
Все тесты в этой статье не пройдут после добавления этого конструктора. Точнее, testPartialMock() приводит к следующей ошибке:
|
1
2
3
|
Missing argument 1 for Calculator::__construct(),
called in /usr/share/php/PHPUnit/Framework/MockObject/Generator.php
on line 224 and defined
|
PHPUnit пытается смоделировать реальный объект, автоматически вызывая конструктор, ожидая, что параметры будут правильно установлены. Есть два пути решения этой проблемы: либо установить параметры, либо не вызывать конструктор.
|
1
2
3
4
5
|
//Specify Constructor Parameters
$phpMock = $this->getMock(‘Calculator’, array(‘add’), array(1,2));
//Do not call original constructor
$phpMock = $this->getMock(‘Calculator’, array(‘add’), array(), », false);
|
Издевательство автоматически обходит эту проблему. Нельзя указывать параметр конструктора; Издевательство просто не вызовет конструктор. Но вы можете указать список параметров конструктора для использования в Mockery. Например:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
function testMockeryConstructorParameters() {
$result = 6;
// Mockery
// Do not call constructor
$noConstrucCall = \Mockery::mock(‘Calculator[add]’);
$noConstrucCall->shouldReceive(‘add’)->andReturn($result);
// Use constructor parameters
$withConstructParams = \Mockery::mock(‘Calculator[add]’, array(1,2));
$withConstructParams->shouldReceive(‘add’)->andReturn($result);
// User real object with real values and mock over it
$realCalculator = new Calculator(1,2);
$mockRealObj = \Mockery::mock($realCalculator);
$mockRealObj->shouldReceive(‘add’)->andReturn($result);
}
|
Технические соображения
Mockery — это еще одна библиотека, которая интегрирует ваши тесты, и вы можете подумать, какие технические последствия это может иметь.
- Издевательство использует много памяти. Вам придется увеличить максимальную память до 512 МБ, если вы хотите запустить много тестов (скажем, более 1000 тестов с более чем 3000 утверждений). Смотрите документацию
php.iniдля более подробной информации. - Вы должны организовать свои тесты для запуска в отдельных процессах, когда имитируете статические методы и вызовы статических методов.
- Вы можете автоматически загружать Mockery в каждый тест, используя функцию начальной загрузки PHPUnit (полезно, когда у вас много тестов, и вы не хотите повторяться).
- Вы можете автоматизировать вызов
\Mockery::close()вtearDown()каждого теста, отредактировавphpunit.xml.
Заключительные выводы
У PHPUnit, безусловно, есть свои проблемы, особенно когда речь идет о функциональности и выразительности. Насмешка может значительно улучшить ваш опыт издевательства, делая ваши тесты простыми в написании и понимании — но это не идеально (такого не бывает!).
В этом уроке освещены многие ключевые аспекты Mockery, но, честно говоря, мы едва поцарапали поверхность. Обязательно изучите репозиторий Github проекта, чтобы узнать больше.
Спасибо за прочтение!