cakephp удалить - Возврат PHPUnitValueMap не дает ожидаемых результатов





теги содержимым (4)


Проблема, похоже, лежит либо где-то еще в коде, либо может быть проблемой со старой версией PHPUnit.

Для меня это работает с использованием этого кода:

cat EnterpriseTest.php

<?php

class EnterpriseTest extends PHPUnit_Framework_TestCase {

    public function testReturnValueMap() {
        $enterprise = $this->getMock('Enterprise', array('field'));
        $enterprise->expects($this->any())
            ->method('field')
            ->will($this->returnValueMap(array(
                array('subscription_id', null),
                array('name', 'Monday Farms')
            )))
        ;
        $enterprise->subscribe('basic');        
    }

}

class Enterprise {

    public function field($name) {
    }

    public function subscribe() {
        echo 'Subscription ID: ';
        var_dump($this->field('subscription_id'));
        echo 'Name: ';
        var_dump($this->field('name'));
    }

}

Вывод:

phpunit-dev EnterpriseTest.php 
PHPUnit 3.7.6 by Sebastian Bergmann.

.Subscription ID: NULL
Name: string(12) "Monday Farms"


Time: 0 seconds, Memory: 6.75Mb

OK (1 test, 1 assertion)

Поэтому, хотя я не могу сказать, почему это не работает, я могу хотя бы сказать вам, что вы делаете это правильно и что вы правильно поняли returnValueMap :)

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

returnValueMap ()

$enterprise = $this->getMock('Enterprise', array('field'));
$enterprise->expects($this->any())
    ->method('field')
    ->will($this->returnValueMap(array(
        array('subscription_id', null),
        array('name', 'Monday Farms')
    )));
$enterprise->subscribe('basic');

Результаты:

Subscription ID: NULL
Name: NULL

returnCallback ()

$enterprise = $this->getMock('Enterprise', array('field'));
$enterprise->expects($this->any())
    ->method('field')
    ->will($this->returnCallback(function ($arg) {
        $map = array(
            'subscription_id' => null,
            'name' => 'Monday Farms'
        );
        return $map[$arg];
    }));
$enterprise->subscribe('basic');

Результаты:

Subscription ID: NULL
Name: string(12) "Monday Farms"

Предприятие :: подписаться

public function subscribe() {
    echo 'Subscription ID: ';
    var_dump($this->field('subscription_id'));
    echo 'Name: ';
    var_dump($this->field('name'));
}

Почему не возвращает функциюValueMap (), как я ожидаю? Что именно мне не хватает?




Я искал эту же проблему и, в конечном счете, из отчаяния xdebug, шаг за шагом через Framework / MockObject / Stub / ReturnValueMap.php и Framework / MockObject / InvocationMocker.php [в методе InvocationMocker :: invoke (PHPUnit_Framework_MockObject_Invocation $ invocation)] и Я заметил следующие моменты:

  1. Мало того, что значения в массиве map, которые вы поставляете, будут такими же, как ожидаемые параметры при вызове функции-заглушки, но они ДОЛЖНЫ БЫТЬ ОДНОГО ТИПА. Это связано с тем, что внутри Framework / MockObject / Stub / ReturnValueMap.php в методе ReturnValueMap :: invoke () сравнение между параметрами и ожидаемыми параметрами сравнивается по строке 75 следующим образом:

    if ($invocation->parameters === $map) {
    

    Итак, ожидаемый результат

    $mock->myStubbedMethod( "1", "2" )
    

    используя карту-массив

    array(
        array( 1, 1, 500 ),
        array( 1, 2, 700 )
    )
    

    разочарует. Вместо этого результатом будет NULL.

  2. В гораздо более тонкой точке вы могли бы опустить метод, используя два разных сценария (как и я - да, глупый!)

    Итак, чтобы выяснить, первый макет-заглушка может содержать

    $mock->expects( $this->any() )
           ->method('myStubbedMethod')
           ->will( $this->returnValue(750) );
    

    а затем позже, в том же методе единицы измерения, он может содержать

    $arrMap = array(
        array( 1, 1, 500 ),
        array( 1, 2, 700 ),
        array( 2, 3, 1500 ),
    );
    $mock->expects( $this->any() )
            ->method('myStubbedMethod')
            ->will( $this->returnValueMap($arrMap) );
    

    Когда вызывается метод stubbed-method, будет реализована версия массива карт. Это очевидно и само собой разумеется, однако при кодировании в разгар моментов и при раздельном разном кодовом поведении в вашем сознании, когда вы развиваетесь, его легко упускать из виду.




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

Пример функции из Zend Framework:

public function getParam($key, $default = null)
{
    $key = (string) $key;
    if (isset($this->_params[$key])) {
        return $this->_params[$key];
    }

    return $default;
}

Должен отображаться следующим образом:

$request->expects($this->any())
        ->method('getParam')
        ->will($this->returnValueMap(array(array($param, null, $value))));

Без нуля в середине это не сработает.




Таким образом, PHP использует нечто, называемое «поздним связыванием». По сути, наследование и определение класса не происходит до конца компиляции файла.

Есть ряд причин для этого. Первый - это пример, который вы показали ( first extends second {} работу). Вторая причина - opcache.

Чтобы компиляция корректно работала в области opcache, компиляция должна происходить без состояния из других скомпилированных файлов. Это означает, что при компиляции файла таблица символов класса очищается.

Затем результат этой компиляции кэшируется. Затем во время выполнения, когда скомпилированный файл загружается из памяти, opcache запускает последнее связывание, которое затем наследует и фактически объявляет классы.

class First {}

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

class Third extends Second {}

Когда это видно, оно скомпилировано, но фактически не объявлено. Вместо этого он добавляется в список «позднего связывания».

class Second extends First {}

Когда это, наконец, видно, оно также скомпилировано и фактически не объявлено. Он добавлен в список поздних ссылок, но после Third .

Итак, теперь, когда происходит поздний процесс привязки, он идет по списку «поздних связанных» классов один за другим. Первый, который он видит, - Third . Затем он пытается найти Second класс, но не может (поскольку он пока еще не объявлен). Таким образом, ошибка возникает.

Если вы переустановите классы:

class Second extends First {}
class Third extends Second {}
class First {}

Тогда вы увидите, что все работает нормально.

Зачем это вообще?

Ну, PHP смешно. Представим себе ряд файлов:

<?php // a.php
class Foo extends Bar {}

<?php // b1.php
class Bar {
    //impl 1
}

<?php // b2.php
class Bar {
    //impl 2
}

Теперь, какой конечный экземпляр Foo вы получите, будет зависеть от того, какой файл b вы загрузили. Если вам понадобится b2.php вы получите Foo extends Bar (impl2) . Если вам понадобится b1.php , вы получите Foo extends Bar (impl1) .

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

В обычном PHP-запросе это тривиально. Причина в том, что мы можем знать о Bar пока мы компилируем Foo . Таким образом, мы можем соответствующим образом скорректировать наш процесс компиляции.

Но когда мы добавляем кеш-код в коде, все становится намного сложнее. Если мы скомпилировали Foo с глобальным состоянием b1.php , то позже (в другом запросе) переключится на b2.php , все будет b2.php странными способами.

Таким образом, вместо того, чтобы компилировать файл, кеши opcode обнуляют глобальное состояние. Таким образом, a.php будет скомпилирован, как если бы это был единственный файл в приложении.

После компиляции он кэшируется в память (для повторного использования более поздними запросами).

Затем, после этой точки (или после ее загрузки из памяти в будущем запросе), происходят «отложенные» шаги. Затем он связывает скомпилированный файл с состоянием запроса.

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

Исходный код.

Чтобы понять, почему, давайте посмотрим на исходный код.

В Zend/zend_compile.c мы видим функцию, которая компилирует класс: zend_compile_class_decl() . Примерно наполовину вниз вы увидите следующий код:

if (extends_ast) {
    opline->opcode = ZEND_DECLARE_INHERITED_CLASS;
    opline->extended_value = extends_node.u.op.var;
} else {
    opline->opcode = ZEND_DECLARE_CLASS;
}

Поэтому он изначально выдает код операции, чтобы объявить унаследованный класс. Затем, после компиляции, zend_do_early_binding() функция, называемая zend_do_early_binding() . Это предварительно объявляет функции и классы в файле (поэтому они доступны вверху). Для обычных классов и функций он просто добавляет их в таблицу символов (объявляет их).

Интересный бит находится в унаследованном случае:

if (((ce = zend_lookup_class_ex(Z_STR_P(parent_name), parent_name + 1, 0)) == NULL) ||
    ((CG(compiler_options) & ZEND_COMPILE_IGNORE_INTERNAL_CLASSES) &&
    (ce->type == ZEND_INTERNAL_CLASS))) {
    if (CG(compiler_options) & ZEND_COMPILE_DELAYED_BINDING) {
        uint32_t *opline_num = &CG(active_op_array)->early_binding;

        while (*opline_num != (uint32_t)-1) {
            opline_num = &CG(active_op_array)->opcodes[*opline_num].result.opline_num;
        }
        *opline_num = opline - CG(active_op_array)->opcodes;
        opline->opcode = ZEND_DECLARE_INHERITED_CLASS_DELAYED;
        opline->result_type = IS_UNUSED;
        opline->result.opline_num = -1;
    }
    return;
}

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

Затем он копирует код операции для объявления класса в задержанный ранний массив привязки.

Наконец, zend_do_delayed_early_binding() функция zend_do_delayed_early_binding() (обычно с помощью opcache), которая проходит через список и фактически связывает унаследованные классы:

while (opline_num != (uint32_t)-1) {
    zval *parent_name = RT_CONSTANT(op_array, op_array->opcodes[opline_num-1].op2);
    if ((ce = zend_lookup_class_ex(Z_STR_P(parent_name), parent_name + 1, 0)) != NULL) {
        do_bind_inherited_class(op_array, &op_array->opcodes[opline_num], EG(class_table), ce, 0);
    }
    opline_num = op_array->opcodes[opline_num].result.opline_num;
}

TL; DR

Заказ не имеет значения для классов, которые не распространяются на другой класс.

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





php cakephp phpunit