Статьи

Самостоятельная инициализация подделок в PHP

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

Как эта архитектура упрощает тестирование? Он делит ваш тестовый код на две категории:

  • интеграционные тесты для адаптеров, которые запускаются не очень часто и проверяют, что интеграция с внешним ресурсом все еще работает.
  • Модульные или функциональные тесты для компонентов приложения, которые используют Test Double ( Stub , Mock или Fake ) вместо реального адаптера.

В этой статье я должен предположить, что вы уже знаете наизусть предыдущий абзац.

Эта проблема

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

Более того, написание фейка обычно очень скучно и требует сохранения гигантских ответов. Например, вот подделка, представляющая адаптер для веб-службы Google Maps Directions:

<DirectionsResponse>
    <status>OK</status>
    <route>
        <summary>A1</summary>
        <leg>
            <step>
                <travel_mode>DRIVING</travel_mode>
                <start_location>
                    <lat>45.4636800</lat>
                    <lng>9.1881700</lng>
                </start_location>
                <end_location>
                    <lat>45.4604600</lat>
                    <lng>9.1816700</lng>
                </end_location>
...this goes on and on for some kilobytes

Эти ответы могут легко не синхронизироваться с реальными, но мы не можем использовать реальный сервис для каждого модульного теста, поскольку это снизит скорость тестов на порядок (по крайней мере).

Решение

Таким образом, мы можем специализировать нашу Fake на самоинициализирующейся Fake, которая обеспечит альтернативную реализацию с помощью реальной. В нашем случае Google Maps, Fake будет использовать реальный веб-сервис для своего первого ответа и поддерживать внутренний кеш . Этот механизм обеспечивает страхование несинхронизированных ответов и позволяет вам наслаждаться скоростью модульных тестов после первоначального прогрева: если вы всегда используете одни и те же данные, повторные запросы к внешнему ресурсу не выполняются.

пример

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

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

<?php
class SelfInitializingFakeTest extends PHPUnit_Framework_TestCase
{
    public function testReturnsAlwaysTheSameResultForEachQuery()
    {
        $fake = new GoogleMapsDirectionsSelfInitializingFake();
        $httpStart = $this->currentTime();
        $fake->getDirections('Milan', 'Rome');
        $httpEnd = $this->currentTime();
        $fake->getDirections('Milan', 'Rome');
        $cachedEnd = $this->currentTime();
        $httpTime = $httpEnd - $httpStart;
        $cachedTime = $cachedEnd - $httpEnd;
        $this->assertGreaterThan(10, $httpTime / $cachedTime);
    }

    private function currentTime()
    {
        return microtime(true);
    }
}

class GoogleMapsDirectionsSelfInitializingFake
{
    public function getDirections($from, $to)
    {
        $url = "http://maps.googleapis.com/maps/api/directions/xml?origin={$from}&destination={$to}&sensor=false";
        $response = file_get_contents($url);
        return new SimpleXMLElement($response);
    }
}

Как и следовало ожидать, тест не пройден:

[10:08:08][giorgio@Desmond:~]$ phpunit SelfInitializingFakeTest.php
PHPUnit 3.5.13 by Sebastian Bergmann.

F

Time: 1 second, Memory: 3.50Mb

There was 1 failure:

1) SelfInitializingFakeTest::testReturnsAlwaysTheSameResultForEachQuery
Failed asserting that <double:1.1485217269437> is greater than <integer:10>.

/home/giorgio/SelfInitializingFakeTest.php:14

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

Вводя кеш:

class GoogleMapsDirectionsSelfInitializingFake
{
    private $cache = array();

    public function getDirections($from, $to)
    {
        $url = "http://maps.googleapis.com/maps/api/directions/xml?origin={$from}&destination={$to}&sensor=false";
        if (isset($this->cache[$url])) {
            $response = $this->cache[$url];
        } else {
            $response = file_get_contents($url);
            $this->cache[$url] = $response;
        }
        return new SimpleXMLElement($response);
    }
}

… мы сдаем тест Будучи потенциально недетерминированным тестом, включающим время выполнения, я неоднократно пытался поймать возможные ложные негативы:

[10:11:20][giorgio@Desmond:~/code/practical-php-testing-patterns]$ phpunit --repeat 100 SelfInitializingFakeTest.php
PHPUnit 3.5.13 by Sebastian Bergmann.

................................................................... 67 / 1 (6700%)
.................................

Time: 48 seconds, Memory: 3.50Mb

OK (100 tests, 100 assertions)

 

Теперь у нас есть Fake, который вы можете сохранить (асимптотически) то же время выполнения независимо закодированного Fake. Но это реализовано в 10 строках кода!