Статьи

Издевательство: лучший путь

Mockery — это расширение PHP, которое предлагает превосходные возможности для насмешек, особенно по сравнению с PHPUnit . Несмотря на то, что среда пересмотра PHPUnit является мощной, Mockery предлагает более естественный язык с набором совпадений, подобным Hamcrest. В этой статье я буду сравнивать две фреймворки и расскажу о лучших возможностях Mockery.

Mockery предлагает набор связанных с насмешками совпадений, которые очень похожи на словарь Hamcrest, предлагая очень естественный способ выразить насмешливые ожидания. Насмешка не переопределяет и не конфликтует со встроенными насмешливыми функциями PHPUnit; фактически вы можете использовать их одновременно (и даже в одном и том же методе тестирования).


Есть несколько способов установить Mockery; Вот самые распространенные методы.

Создайте файл с именем 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 упрощают установку 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 проекта, чтобы узнать больше.

Спасибо за прочтение!