Как unit test вызов curl в php

Как бы вы поделились модулем тестирования реализации завитка?

  public function get() {
    $ch = curl_init($this->request->getUrl());

    curl_setopt($ch, CURLOPT_HEADER, false);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

    $result = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
    curl_close($ch);

    if (!strstr($type, 'application/json')) {
      throw new HttpResponseException('JSON response not found');
    }

    return new HttpResponse($code, $result);
  }

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

Ответ 1

Как предложил thomasrutter, создайте класс для абстрактного использования функций cURL.

interface HttpRequest
{
    public function setOption($name, $value);
    public function execute();
    public function getInfo($name);
    public function close();
}

class CurlRequest implements HttpRequest
{
    private $handle = null;

    public function __construct($url) {
        $this->handle = curl_init($url);
    }

    public function setOption($name, $value) {
        curl_setopt($this->handle, $name, $value);
    }

    public function execute() {
        return curl_exec($this->handle);
    }

    public function getInfo($name) {
        return curl_getinfo($this->handle, $name);
    }

    public function close() {
        curl_close($this->handle);
    }
}

Теперь вы можете протестировать, используя макет интерфейса HttpRequest, не вызывая никаких функций cURL.

public function testGetThrowsWhenContentTypeIsNotJson() {
    $http = $this->getMock('HttpRequest');
    $http->expects($this->any())
         ->method('getInfo')
         ->will($this->returnValue('not JSON'));
    $this->setExpectedException('HttpResponseException');
    // create class under test using $http instead of a real CurlRequest
    $fixture = new ClassUnderTest($http);
    $fixture->get();
}

Изменить Исправлена ​​простая ошибка синтаксического анализа PHP.

Ответ 2

Не используйте завиток напрямую, но через оболочку, такую ​​как PEAR HTTP_Request2. С его помощью у вас есть возможность обмениваться драйвером curl с макетным драйвером - идеально подходит для модульных тестов.

Ответ 3

Я наткнулся на этот вопрос, когда я пытался проверить класс, используя сам cURL. Я принял совет Дэвида Харкнесса к сердцу и создал интерфейс для cURL. Тем не менее, функция заглушки/макета, предоставленная PHPUnit, в моем случае была недостаточной, поэтому я добавил свою собственную реализацию заглушки интерфейса и поместил все это в GitHub. И поскольку этот вопрос проявляется довольно рано в Google при поиске этой проблемы, я думал, что разместил его здесь, чтобы другие могли сэкономить усилия.

Вот он.

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

Интерфейс представляет собой 1:1 отображение функций PHP cURL, чтобы было очень легко начать использовать интерфейс (просто передайте ClassUnderTest экземпляр, реализующий SAI_CurlInterface, а затем вызовите все функции cURL, как и раньше, но как методы в этом случае). Класс SAI_Curl реализует этот интерфейс, просто делегируя cURL. Теперь, если вы хотите протестировать ClassUnderTest, вы можете дать ему экземпляр SAI_CurlStub.

Заглушка в основном устраняет проблему, из-за которой PHPUnit mocks и заглушки не могут возвращать фиктивные данные в зависимости от предыдущих вызовов функций (но так работает cURL - вы настраиваете свои параметры, а ответ, код ошибки и cURL-info зависят от тех опции). Итак, вот краткий пример, показывающий эти возможности для ответов (для кодов ошибок и cURL-info, см. Wiki).

public function testGetData()
{
    $curl = new SAI_CurlStub();

    // Set up the CurlStub
    $defaultOptions = array(
        CURLOPT_URL => 'http://www.myserver.com'
    );
    $chromeOptions = array(
        CURLOPT_URL => 'http://www.myserver.com',
        CURLOPT_USERAGENT => 'Chrome/22.0.1207.1'
    );
    $safariOptions = array(
        CURLOPT_URL => 'http://www.myserver.com',
        CURLOPT_USERAGENT => 'Safari/537.1'
    );

    $curl->setResponse('fallback response');
    $curl->setResponse('default response from myserver.com'
                       $defaultOptions);
    $curl->setResponse('response for Chrome from myserver.com',
                       $chromeOptions);
    $curl->setResponse('response for Safari from myserver.com',
                       $safariOptions);

    $cut = new ClassUnderTest($curl);

    // Insert assertions to check whether $cut handles the
    // different responses correctly
    ...
}

Вы можете сделать свой ответ зависимым от любой комбинации любых параметров cURL. Конечно, вы можете это сделать еще дальше. Скажем, например, ваш ClassUnderTest принимает некоторые данные XML с сервера и анализирует его (ну, у вас должно быть два отдельных класса для этих задач, но допустим это для нашего примера), и вы хотите проверить это поведение. Вы можете загрузить ответ XML вручную, и попросите свой тест прочитать данные из файла и передать его в ответ. Тогда вы точно знаете, какие данные есть, и можете проверить, правильно ли он разбирается. Кроме того, вы можете реализовать SAI_CurlInterface загрузку всех ответов из вашей файловой системы сразу, но существующая реализация, безусловно, является точкой начала.

В то время, когда я пишу этот ответ, @SAI_CurlStub @пока не поддерживает функции cURL multi-lib, но я планирую реализовать это тоже в будущем.

Я надеюсь, что этот заглушка поможет всем, кто хочет unit test cURL-зависимые классы. Не стесняйтесь проверять и использовать классы, или вносить свой вклад, конечно же - это на GitHub в конце концов:). Кроме того, я открыт для любой конструктивной критики относительно реализации и использования интерфейса и заглушки.

Ответ 4

Один подход к этому заключается в замене используемого интерфейса (в данном случае, функций curl_) с фиктивными версиями самих себя, которые возвращают определенные значения. Если бы вы использовали объектно-ориентированную библиотеку, это было бы проще, потому что вы могли бы просто заменить фиктивный объект, который имеет те же имена методов (и, действительно, фреймворки, такие как simpletest, могут легко настроить методы фиктивных объектов). В противном случае, возможно, есть и другое колдовство, которое вы можете использовать для переопределения встроенных функций с манекенами. Это расширение включает override_function(), которое выглядит так, как вам нужно, хотя это добавит еще одну зависимость.

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

Кстати, PHP 5.4 фактически будет включать собственный веб-сервер, который пригодится для этой цели. В противном случае вы можете поместить тест script на известный сервер, которым вы управляете, или распределить простую конфигурацию сервера с вашими тестами.

Если вы использовали фактический сервер для тестирования, это станет меньше unit test и более тестов интеграции, поскольку вы будете тестировать как ваш PHP, так и сервер, и интеграцию между ними. Вы также пропустили бы возможность проверить по требованию, как ваш код обрабатывает определенные сбои.

Ответ 5

Вы можете использовать библиотеку функций mock. Я сделал один для вас: php-mock-phpunit

namespace foo;

use phpmock\phpunit\PHPMock;

class BuiltinTest extends \PHPUnit_Framework_TestCase
{

    use PHPMock;

    public function testCurl()
    {
        $curl_exec = $this->getFunctionMock(__NAMESPACE__, "curl_exec");
        $curl_exec->expects($this->once())->willReturn("body");

        $ch = curl_init();
        $this->assertEquals("body", curl_exec($ch));
    }
}

Ответ 6

В unit test, request->getUrl() вернуть URI локального файла, который, как вы знаете, будет генерировать исключение.