Статьи

Все о насмешках с помощью PHPUnit

Существует два стиля тестирования: «черный ящик» и «белый ящик». Тестирование черного ящика фокусируется на состоянии объекта; в то время как тестирование белого ящика фокусируется на поведении. Два стиля дополняют друг друга и могут быть объединены для тщательного тестирования кода. Mocking позволяет нам тестировать поведение, и этот учебник объединяет концепцию mocking с TDD для создания примера класса, который использует несколько других компонентов для достижения своей цели.


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

Совокупность всех методов, открытых и закрытых, доступных через открытые методы, представляет поведение объекта. Например, указание объекту перемещаться заставляет этот объект взаимодействовать не только с его внутренними методами, но и с другими объектами. С точки зрения пользователя, объект имеет только одно простое поведение: он движется .

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

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


Наш проверенный класс никогда не использует эти фиктивные объекты.

Давайте представим, что мы создаем программу для дистанционного управления игрушечной машиной. Все команды нашего класса поступают через пульт дистанционного управления. Мы должны создать класс, который понимает, что посылает пульт дистанционного управления, и выдает команды на автомобиль.

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


Вот полная схема приложения. Там нет никакого объяснения на данный момент; просто имейте это в виду для дальнейшего использования.


Тестовая заглушка — это объект для управления косвенным вводом тестируемого кода.

Mocking — это стиль тестирования, который требует своего собственного набора инструментов, набора специальных объектов, представляющих различные уровни имитации поведения объекта. Эти:

  • фиктивные объекты
  • тестовые заглушки
  • тестовые шпионы
  • тестовые макеты
  • тестовые подделки

Каждый из этих объектов имеет свою особую область действия и поведение. В PHPUnit они создаются с помощью метода $this->getMock() . Разница в том, как и по каким причинам используются объекты.

Чтобы лучше понять эти объекты, я буду постепенно реализовывать «Контроллер игрушечного автомобиля», используя типы объектов в порядке, указанном выше. Каждый объект в списке является более сложным, чем объект перед ним. Это приводит к реализации, которая радикально отличается от реальной. Кроме того, будучи воображаемым приложением, я буду использовать некоторые сценарии, которые могут быть даже невозможны в реальной игрушечной машине. Но давайте представим, что они нам нужны, чтобы понять общую картину.


Фиктивные объекты — это объекты, от которых зависит тестируемая система (SUT), но на самом деле они никогда не используются. Фиктивный объект может быть аргументом, переданным другому объекту, или он может быть возвращен вторым объектом и затем передан третьему объекту. Дело в том, что наш проверенный класс никогда не использует эти фиктивные объекты. В то же время объект должен напоминать реальный объект; в противном случае получатель может отказаться от него.

Лучший способ проиллюстрировать это — представить сценарий; схема которого ниже:

Фиктивный объект в контекстной схеме

Оранжевый объект — это RemoteControlTranslator . Его главная цель — получать сигналы с пульта и переводить их в сообщения для наших классов. В какой-то момент пользователь выполнит действие «Готов к работе» на пульте дистанционного управления. Переводчик получит сообщение и создаст классы, необходимые для подготовки машины к работе.

Производитель сказал, что «Готов к работе» означает, что двигатель запущен, коробка передач находится в нейтральном положении, а огни включены или выключаются в соответствии с запросом пользователя.

Это означает, что пользователь может предварительно определить состояние источников света, прежде чем они будут готовы к работе, и они будут включаться или выключаться на основе этого предварительно определенного значения при активации. Затем RemoteControlTranslator отправляет всю необходимую информацию CarControl класса getReadyToGo($engine, $gearbox, $electronics, $lights) . Я знаю, что это далеко от идеального дизайна и нарушает несколько принципов и шаблонов, но это очень хорошо для этого примера.

Начните наш проект с этой исходной файловой структурой:

Исходная файловая структура

Помните, что все классы в папке CarInterface предоставляются производителем автомобиля; мы не знаем их реализацию. Все, что мы знаем, это подписи классов, но нас это не волнует.

Наша главная цель — реализовать класс CarController . Чтобы протестировать этот класс, нам нужно представить, как мы хотим его использовать. Другими словами, мы ставим себя на место RemoteControlTranslator и / или любого другого будущего класса, который может использовать CarController . Давайте начнем с создания кейса для нашего класса.

1
2
class CarControllerTest extends PHPUnit_Framework_TestCase {
}

Затем добавьте метод тестирования.

1
2
function testItCanGetReadyTheCar() {
   }

Теперь подумайте, что нам нужно передать getReadyToGo() : двигатель, коробка передач, контроллер электроники и легкая информация. Ради этого примера мы будем только издеваться над огнями:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
require_once ‘../CarController.php’;
include ‘../autoloadCarInterfaces.php’;
 
class CarControllerTest extends PHPUnit_Framework_TestCase {
 
    function testItCanGetReadyTheCar() {
        $carController = new CarController();
 
        $engine = new Engine();
        $gearbox = new Gearbox();
        $electornics = new Electronics();
 
        $dummyLights = $this->getMock(‘Lights’);
 
        $this->assertTrue($carController->getReadyToGo($engine, $gearbox, $electornics, $dummyLights));
    }
 
}

Это очевидно потерпит неудачу с:

1
PHP Fatal error: Call to undefined method CarController::getReadyToGo()

Несмотря на неудачу, этот тест дал нам отправную точку для нашей реализации CarController . Я включил файл с именем autoloadCarInterfaces.php , которого не было в первоначальном списке. Я понял, что мне нужно что-то для загрузки классов, и я написал очень простое решение. Мы всегда можем переписать его, когда предоставляются реальные классы, но это совсем другая история. Сейчас мы будем придерживаться простого решения:

1
2
3
4
5
6
7
foreach (scandir(dirname(__FILE__) . ‘/CarInterface’) as $filename) {
    $path = dirname(__FILE__) .
 
    if (is_file($path)) {
        require_once $path;
    }
}

Я предполагаю, что этот загрузчик классов очевиден для всех; Итак, давайте обсудим тестовый код.

Сначала мы создаем экземпляр CarController , класс, который мы хотим протестировать. Затем мы создаем экземпляры всех других классов, которые нас интересуют: двигатель, коробка передач и электроника.

Затем мы создаем фиктивный объект Lights , вызывая метод getMock() PHPUnit и передавая имя класса Lights . Это возвращает экземпляр Lights , но каждый метод возвращает null фиктивный объект. Этот фиктивный объект не может ничего делать, но он дает нашему коду интерфейс, необходимый для работы с объектами Light .

Очень важно отметить, что $dummyLights — это объект Lights , и любой пользователь, ожидающий объект Light может использовать фиктивный объект, не зная, что это не настоящий объект Lights .

Чтобы избежать путаницы, я рекомендую указывать тип параметра при определении функции. Это заставляет среду выполнения PHP проверять аргументы, переданные функции. Без указания типа данных вы можете передать любой объект любому параметру, что может привести к сбою вашего кода. Имея это в виду, давайте рассмотрим класс Electronics :

1
2
3
4
5
6
7
require_once ‘Lights.php’;
 
class Electronics {
 
    function turnOn(Lights $lights) {}
 
}

Давайте реализуем тест:

01
02
03
04
05
06
07
08
09
10
11
12
class CarController {
 
    function getReadyToGo(Engine $engine, Gearbox $gearbox, Electronics $electronics, Lights $lights) {
        $engine->start();
        $gearbox->shift(‘N’);
 
        $electronics->turnOn($lights);
 
        return true;
    }
 
}

Как вы можете видеть, getReadyToGo() использовала объект $lights только для того, чтобы отправить его в метод turnOn() объекта $electronics . Это идеальное решение для такой ситуации? Возможно, нет, но вы можете четко наблюдать, как фиктивный объект, не имеющий никакого отношения к функции getReadyToGo() , передается одному объекту, который действительно нуждается в нем.

Обратите внимание, что все классы, содержащиеся в каталоге CarInterface, предоставляют фиктивные объекты при инициализации. Также предположим, что для этого упражнения мы ожидаем, что производитель предоставит реальные занятия в будущем. Мы не можем полагаться на их текущее отсутствие функциональности; поэтому мы должны гарантировать, что наши тесты пройдены.


Тестовая заглушка — это объект для управления косвенным вводом тестируемого кода. Но что такое косвенный вклад? Это источник информации, который не может быть указан напрямую.

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

Шпионы, по определению, более способные окурки.

Данные могут быть получены только путем запроса конкретного объекта, и во многих случаях эти объекты используются для определенной цели внутри тестируемого класса. Мы не хотим «новый» ( new SomeClass() ) класс внутри другого класса для целей тестирования. Поэтому нам нужно внедрить экземпляр класса, который действует как SomeClass без внедрения фактического объекта SomeClass .

Нам нужен класс-заглушка, который затем приводит к внедрению зависимости . Внедрение зависимостей (DI) — это метод, который внедряет объект в другой объект, заставляя его использовать введенный объект. DI распространен в TDD и абсолютно необходим практически для любого проекта. Он предоставляет простой способ заставить объект использовать подготовленный к тесту класс вместо реального класса, используемого в производственной среде.

Давайте заставим нашу игрушечную машинку двигаться вперед.

Тестовая заглушка в контекстной схеме

Мы хотим реализовать метод с именем moveForward() . Этот метод сначала запрашивает у объекта StatusPanel состояние топлива и двигателя. Если автомобиль готов к работе, метод дает указание электронике ускоряться.

Чтобы лучше понять, как работает заглушка, я сначала напишу код для проверки состояния и ускорения:

1
2
3
4
5
function goForward(Electronics $electronics) {
       $statusPanel = new StatusPanel();
       if($statusPanel->engineIsRunning() && $statusPanel->thereIsEnoughFuel())
           $electronics->accelerate ();
   }

Этот код довольно прост, но у нас нет реального движка или топлива для тестирования нашей реализации goForward() . Наш код даже не StatusPanel оператор if потому что у нас нет класса StatusPanel . Но если мы продолжим тестирование, логическое решение начнет появляться:

01
02
03
04
05
06
07
08
09
10
11
function testItCanAccelerate() {
       $carController = new CarController();
 
       $electronics = new Electronics();
 
       $stubStatusPanel = $this->getMock(‘StatusPanel’);
       $stubStatusPanel->expects($this->any())->method(‘thereIsEnoughFuel’)->will($this->returnValue(TRUE));
       $stubStatusPanel->expects($this->any())->method(‘engineIsRunning’)->will($this->returnValue(TRUE));
 
       $carController->goForward($electronics, $stubStatusPanel);
   }

Построчное объяснение:

Я люблю рекурсию; всегда проще проверить рекурсию, чем циклы.

  • создать новый CarController
  • создать зависимый объект Electronics
  • создать макет для StatusPanel
  • ожидаем, что вызову thereIsEnoughFuel() ноль или более раз и вернем true
  • ожидать вызова engineIsRunning() ноль или более раз и вернуть true
  • вызовите goForward() с объектом Electronics и StubbedStatusPanel

Это тест, который мы хотим написать, но он не будет работать с нашей текущей реализацией goForward() . Мы должны изменить это:

1
2
3
4
5
function goForward(Electronics $electronics, StatusPanel $statusPanel = null) {
       $statusPanel = $statusPanel ?
       if($statusPanel->engineIsRunning() && $statusPanel->thereIsEnoughFuel())
           $electronics->accelerate ();
   }

Наша модификация использует внедрение зависимостей , добавляя второй необязательный параметр типа StatusPanel . Мы определяем, имеет ли этот параметр значение, и создаем новую StatusPanel если $statusPanel имеет значение null. Это гарантирует, что новый объект StatusPanel будет создан в StatusPanel и в то же время позволит нам протестировать метод.

Важно указать тип параметра $statusPanel . Это гарантирует, что в StatusPanel может быть передан только объект StatusPanel (или объект унаследованного класса). Но даже с этой модификацией наш тест все еще не завершен.


Мы должны проверить макет объекта Electronics чтобы наш метод из шага 6 вызывал accelerate() . Мы не можем использовать настоящий класс Electronics по нескольким причинам:

  • У нас нет класса.
  • Мы не можем проверить его поведение.
  • Даже если бы мы могли это назвать, мы должны проверить это изолированно.

Тестовый макет — это объект, который способен управлять как косвенным вводом, так и выводом, и он имеет механизм для автоматического утверждения ожиданий и результатов. Это определение может показаться немного запутанным, но его действительно довольно просто реализовать:

01
02
03
04
05
06
07
08
09
10
11
12
function testItCanAccelerate() {
       $carController = new CarController();
 
       $electronics = $this->getMock(‘Electronics’);
       $electronics->expects($this->once())->method(‘accelerate’);
 
       $stubStatusPanel = $this->getMock(‘StatusPanel’);
       $stubStatusPanel->expects($this->any())->method(‘thereIsEnoughFuel’)->will($this->returnValue(TRUE));
       $stubStatusPanel->expects($this->any())->method(‘engineIsRunning’)->will($this->returnValue(TRUE));
 
       $carController->goForward($electronics, $stubStatusPanel);
   }

Мы просто изменили переменную $electronics . Вместо того, чтобы создавать настоящий объект « Electronics , мы просто издеваемся над ним.

На следующей строке мы определяем ожидание для объекта $electronics . Точнее, мы ожидаем, что метод accelerate() вызывается только один раз ( $this->once() ). Тест сейчас проходит!

Не стесняйтесь играть с этим тестом. Попробуйте изменить $this->once() на $this->exactly(2) и посмотрите, какое приятное сообщение об ошибке выдает PHPUnit:

1
2
3
1) CarControllerTest::testItCanAccelerate
Expectation failed for method name is equal to <string:accelerate>;
Method was expected to be called 2 times, actually called 1 times.

Тестовый шпион — это объект, способный захватывать косвенный вывод и предоставлять косвенный ввод по мере необходимости.

Косвенный вывод — это то, что мы не можем наблюдать напрямую. Например: когда тестируемый класс вычисляет значение, а затем использует его в качестве аргумента для метода другого объекта. Единственный способ наблюдать этот вывод — спросить вызываемый объект о переменной, используемой для доступа к его методу.

Это определение делает шпиона почти издевательским.

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

В таком случае, как мы можем создать тестового шпиона, используя PHPUnit getMock() ? Мы не можем (ну, мы не можем создать чистого шпиона), но мы можем создавать насмешки, способные шпионить за другими объектами.

Давайте внедрим систему торможения, чтобы мы могли остановить машину. Торможение действительно простое; пульт дистанционного управления будет определять интенсивность торможения от пользователя и отправлять его на контроллер. Пульт дистанционного управления также обеспечивает «Аварийный останов!» кнопка. Это должно немедленно задействовать тормоза с максимальной мощностью.

Тормозная мощность измеряет значения в диапазоне от 0 до 100, где 0 означает ничто, а 100 означает максимальную тормозную мощность. «Аварийная остановка!» Команда будет принята как другой вызов.

Тестовая заглушка в контекстной схеме

CarController выдаст объекту Electronics сообщение для активации тормозной системы. Контроллер автомобиля также может запросить у StatusPanel информацию о скорости, полученную с помощью датчиков на автомобиле.

Давайте сначала реализуем чистый шпионский объект без использования инфраструктуры насмешек PHPUnit. Это поможет вам лучше понять концепцию тестового шпиона. Начнем с проверки подписи объекта Electronics .

1
2
3
4
5
6
7
class Electronics {
 
    function turnOn(Lights $lights) {}
    function accelerate(){}
    function pushBrakes($brakingPower){}
 
}

Нас интересует метод pushBrakes() . Я не назвал это brake() чтобы избежать путаницы с ключевым словом break в PHP.

Чтобы создать настоящего шпиона, мы расширим Electronics и переопределим метод pushBrakes() . Этот переопределенный метод не будет тормозить; вместо этого он будет регистрировать только мощность торможения.

01
02
03
04
05
06
07
08
09
10
11
class SpyingElectronics extends Electronics {
    private $brakingPower;
 
    function pushBrakes($brakingPower) {
        $this->brakingPower = $brakingPower;
    }
 
    function getBrakingPower() {
        return $this->brakingPower;
    }
}

Метод getBrakingPower() дает нам возможность проверить мощность торможения в нашем тесте. Это не тот метод, который мы использовали бы в производстве.

Теперь мы можем написать тест, способный проверить мощность торможения. Следуя принципам TDD, мы начнем с самого простого теста и предоставим самую базовую реализацию:

1
2
3
4
5
6
7
8
9
function testItCanStop() {
       $halfBrakingPower = 50;
       $electronicsSpy = new SpyingElectronics();
 
       $carController = new CarController();
       $carController->pushBrakes($halfBrakingPower, $electronicsSpy);
 
       $this->assertEquals($halfBrakingPower, $electronicsSpy->getBrakingPower());
   }

Этот тест не pushBrakes() потому что у нас еще нет pushBrakes() в CarController . Давайте исправим это и напишем:

1
2
3
function pushBrakes($brakingPower, Electronics $electronics) {
       $electronics->pushBrakes($brakingPower);
   }

Теперь тест проходит, эффективно тестируя метод pushBrakes() .

Мы также можем следить за вызовами методов. Тестирование класса StatusPanel является следующим логическим шагом. Он предоставляет пользователю различную информацию о машине с дистанционным управлением. Давайте напишем тест, который проверяет, StatusPanel объект StatusPanel о скорости автомобиля. Мы создадим шпиона для этого:

01
02
03
04
05
06
07
08
09
10
11
class SpyingStatusPanel extends StatusPanel {
    private $speedWasRequested = false;
 
    function getSpeed() {
        $this->speedWasRequested = true;
    }
 
    function speedWasRequested() {
        return $this->speedWasRequested;
    }
}

Затем мы модифицируем наш тест для использования шпиона:

01
02
03
04
05
06
07
08
09
10
11
function testItCanStop() {
       $halfBrakingPower = 50;
       $electronicsSpy = new SpyingElectronics();
       $statusPanelSpy = new SpyingStatusPanel();
 
       $carController = new CarController();
       $carController->pushBrakes($halfBrakingPower, $electronicsSpy, $statusPanelSpy);
 
       $this->assertEquals($halfBrakingPower, $electronicsSpy->getBrakingPower());
       $this->assertTrue($statusPanelSpy->speedWasRequested());
   }

Обратите внимание, что я не написал отдельный тест.

Хорошо следовать рекомендации «одно утверждение на тест», но когда ваш тест описывает действие, требующее нескольких шагов или состояний, использование более одного утверждения в одном тесте является приемлемым.

Более того, это сохраняет ваши утверждения о единой концепции в одном месте. Это помогает устранить дублирующийся код, не требуя от вас многократно устанавливать одни и те же условия для вашей SUT.

А теперь реализация:

1
2
3
4
5
function pushBrakes($brakingPower, Electronics $electronics, StatusPanel $statusPanel = null) {
       $statusPanel = $statusPanel ?
       $electronics->pushBrakes($brakingPower);
       $statusPanel->getSpeed();
   }

Меня беспокоит лишь маленькая крошечная вещь: имя этого теста testItCanStop() . Это ясно означает, что мы нажимаем на тормоза, пока машина не остановится полностью. Мы, однако, вызвали метод pushBrakes() , что не совсем корректно. Время на рефакторинг:

1
2
3
4
5
function stop($brakingPower, Electronics $electronics, StatusPanel $statusPanel = null) {
       $statusPanel = $statusPanel ?
       $electronics->pushBrakes($brakingPower);
       $statusPanel->getSpeed();
   }

Не забудьте также изменить вызов метода в тесте.

1
$carController->stop($halfBrakingPower, $electronicsSpy, $statusPanelSpy);

Косвенный вывод — это то, что мы не можем наблюдать напрямую.

На данный момент нам нужно подумать о нашей тормозной системе и о том, как она работает. Есть несколько возможностей, но для этого примера предположим, что поставщик игрушечной машины указал, что торможение происходит в дискретных интервалах. Вызов метода pushBreakes() объекта Electronics приводит к pushBreakes() что тормоз pushBreakes() на некоторое время, а затем отпускает его. Интервал времени для нас не важен, но давайте представим, что это доля секунды. С таким небольшим интервалом времени мы должны непрерывно отправлять команды pushBrakes() пока скорость не станет равной нулю.

Шпионы, по определению, являются более способными заглушками, и они могут также контролировать косвенный ввод при необходимости. Давайте сделаем наш шпион StatusPanel более способным и предложим некоторое значение для скорости. Я думаю, что первый звонок должен обеспечить положительную скорость — скажем, значение 1 . Второй звонок обеспечит скорость 0 .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
class SpyingStatusPanel extends StatusPanel {
 
    private $speedWasRequested = false;
    private $currentSpeed = 1;
 
    function getSpeed() {
        if ($this->speedWasRequested) $this->currentSpeed = 0;
        $this->speedWasRequested = true;
        return $this->currentSpeed;
    }
 
    function speedWasRequested() {
        return $this->speedWasRequested;
    }
 
    function spyOnSpeed() {
        return $this->currentSpeed;
    }
 
}

getSpeed() метод getSpeed() возвращает соответствующее значение скорости с помощью spyOnSpeed() . Давайте добавим третье утверждение к нашему тесту:

01
02
03
04
05
06
07
08
09
10
11
12
function testItCanStop() {
       $halfBrakingPower = 50;
       $electronicsSpy = new SpyingElectronics();
       $statusPanelSpy = new SpyingStatusPanel();
 
       $carController = new CarController();
       $carController->stop($halfBrakingPower, $electronicsSpy, $statusPanelSpy);
 
       $this->assertEquals($halfBrakingPower, $electronicsSpy->getBrakingPower());
       $this->assertTrue($statusPanelSpy->speedWasRequested());
       $this->assertEquals(0, $statusPanelSpy->spyOnSpeed());
   }

Согласно последнему утверждению, скорость должна иметь значение скорости 0 после того, как метод stop() завершит выполнение. Выполнение этого теста для нашего производственного кода приводит к ошибке с загадочным сообщением:

1
2
1) CarControllerTest::testItCanStop
Failed asserting that 1 matches expected 0.

Давайте добавим наше собственное сообщение с утверждением:

1
2
$this->assertEquals(0, $statusPanelSpy->spyOnSpeed(),
   ‘Expected speed to be 0 (zero) after stopping but it actually was ‘ .

Это создает гораздо более читаемое сообщение об ошибке:

1
2
3
1) CarControllerTest::testItCanStop
Expected speed to be 0 (zero) after stopping but it actually was 1
Failed asserting that 1 matches expected 0.

Хватит провалов! Давайте сделаем это.

1
2
3
4
5
function stop($brakingPower, Electronics $electronics, StatusPanel $statusPanel = null) {
       $statusPanel = $statusPanel ?
       $electronics->pushBrakes($brakingPower);
       if ($statusPanel->getSpeed()) $this->stop($brakingPower, $electronics, $statusPanel);
   }

Я люблю рекурсию; всегда проще проверить рекурсию, чем циклы. Более простое тестирование означает более простой код, что, в свою очередь, означает лучший алгоритм. Проверьте Премиум-предпосылку Преобразования для получения дополнительной информации по этому вопросу.

Достаточно с дополнительными классами. Давайте перепишем это с помощью фреймворка PHPUnit и удалим этих чистых шпионов. Почему?

Потому что PHPUnit предлагает лучший и простой синтаксис насмешки, меньше кода и несколько хороших предопределенных методов.

Я обычно создаю чистых шпионов и заглушек только тогда, когда издеваться над ними с помощью getMock() будет слишком сложно. Если ваши классы настолько сложны, что getMock() не может их обработать, тогда у вас есть проблема с getMock() , а не с вашими тестами.

01
02
03
04
05
06
07
08
09
10
11
function testItCanStop() {
       $halfBrakingPower = 50;
       $electronicsSpy = $this->getMock(‘Electronics’);
       $electronicsSpy->expects($this->exactly(2))->method(‘pushBrakes’)->with($halfBrakingPower);
       $statusPanelSpy = $this->getMock(‘StatusPanel’);
       $statusPanelSpy->expects($this->at(0))->method(‘getSpeed’)->will($this->returnValue(1));
       $statusPanelSpy->expects($this->at(1))->method(‘getSpeed’)->will($this->returnValue(0));
 
       $carController = new CarController();
       $carController->stop($halfBrakingPower, $electronicsSpy, $statusPanelSpy);
   }

Совокупность всех методов, открытых и закрытых, доступных через открытые методы, представляет поведение объекта.

Построчное объяснение приведенного выше кода:

  • установить половину тормозного усилия = 50
  • создать макет Electronics
  • ожидать, что метод pushBrakes() будет выполнен ровно два раза с указанной выше мощностью торможения
  • создать макет StatusPanel
  • вернуть 1 при первом getSpeed()
  • вернуть 0 при втором выполнении getSpeed()
  • вызовите протестированный метод stop() для реального объекта CarController

Вероятно, самая интересная вещь в этом коде — это метод $this->at($someValue) . PHPUnit подсчитывает количество вызовов для этого макета. Подсчет происходит на фиктивном уровне; поэтому, вызов нескольких методов в $statusPanelSpy счетчик. Поначалу это может показаться немного нелогичным; так что давайте посмотрим на пример.

Предположим, мы хотим проверять уровень топлива при каждом вызове stop() . Код будет выглядеть так:

1
2
3
4
5
6
function stop($brakingPower, Electronics $electronics, StatusPanel $statusPanel = null) {
       $statusPanel = $statusPanel ?
       $electronics->pushBrakes($brakingPower);
       $statusPanel->thereIsEnoughFuel();
       if ($statusPanel->getSpeed()) $this->stop($brakingPower, $electronics, $statusPanel);
   }

Это сломает наш тест. Вы можете быть смущены, почему, но вы получите следующее сообщение:

1
2
3
1) CarControllerTest::testItCanStop
Expectation failed for method name is equal to <string:pushBrakes> when invoked 2 time(s).
Method was expected to be called 2 times, actually called 1 times.

Совершенно очевидно, что pushBrakes() следует вызывать два раза. Почему тогда мы получаем это сообщение? Из-за ожидания $this->at($someValue) . Счетчик увеличивается следующим образом:

  • первый вызов stop() -> первый вызов thereIsEnougFuel() => внутренний счетчик на 0
  • первый вызов stop() -> первый вызов getSpeed() => внутренний счетчик на 1 и возврат 0
  • второй вызов stop() никогда не происходит => второй вызов getSpeed() никогда не происходит

Каждый вызов любого $statusPanelSpy метода в $statusPanelSpy увеличивает внутренний счетчик PHPUnit.


Если публичные методы сродни сообщениям, то частные методы похожи на мысли.

Подделка теста — это более простая реализация объекта производственного кода. Это очень похожее определение для тестирования заглушек. В действительности, Fakes и Stubs очень похожи по внешнему поведению. Оба являются объектами, имитирующими поведение некоторых других реальных объектов, и оба реализуют метод управления косвенным вводом. Разница в том, что подделки гораздо ближе к реальному объекту, чем к фиктивному объекту.

Stub — это в основном фиктивный объект, методы которого возвращают предопределенные значения. Fake, однако, делает полную реализацию реального объекта намного проще. Вероятно, наиболее распространенным примером является InMemoryDatabase для идеального моделирования реального класса базы данных без фактической записи в хранилище данных. Таким образом, тестирование становится быстрее.

Test Fakes не должен реализовывать какие-либо методы для прямого контроля ввода или возврата наблюдаемого состояния. Они не используются для допроса; они используются, чтобы обеспечить — не наблюдать. Наиболее распространенные случаи использования Fakes — это когда реальный зависимый компонент (DOC) еще не записан, он слишком медленный (как база данных) или реальный DOC недоступен в среде тестирования.


Наиболее важной фиктивной функциональностью является управление DOC. Он также предоставляет отличный способ управления косвенным вводом / выводом с помощью методов внедрения зависимостей.

Есть два основных мнения о насмешках:

Некоторые говорят, что издеваться плохо …

  • Некоторые говорят, что издеваться плохо , и они правы. Пересмешка делает что-то неуловимое и уродливое: слишком много тестов привязывает к реализации. По возможности, тест должен быть максимально независимым от реализации. Тестирование черного ящика всегда предпочтительнее, чем тестирование белого ящика. Всегда проверяйте состояние, если можете; не издевайтесь над поведением. Быть анти-mockist поощряет разработку снизу вверх и дизайн. Это означает, что небольшие составные части системы сначала создаются, а затем объединяются в гармоничную структуру.
  • Некоторые говорят, что дразнить это хорошо , и они правы. Насмешка делает что-то тонкое и прекрасное; это определяет поведение. Это заставляет нас думать намного больше с точки зрения пользователя. Мокисты обычно используют нисходящий подход к реализации и дизайну. Они начинают с самого верхнего класса в системе и пишут первый тест, высмеивая какой-то другой воображаемый DOC, который еще не реализован. Компоненты системы появляются и развиваются на основе макетов, созданных на один уровень выше.

При этом вам решать, по какому пути идти.

Некоторые предпочитают быть издевателями, другие предпочитают государственные испытания. У каждого подхода есть свои плюсы и минусы. Полностью смоделированная система предлагает дополнительную поведенческую информацию в тестах. Полностью государственная система предлагает больше деталей о компонентах, но она также может скрывать некоторое поведение.