Мокайте только один метод на заглушках PHPSpec

Итак, я пытаюсь перенести один из своих пакетов в тесты PHPSpec, но вскоре столкнулся с этой проблемой. Пакеты — это пакет корзины покупок, поэтому я хочу проверить, что когда вы добавляете два предмета в корзину, корзина имеет счет два, просто. Но, конечно, в корзине при добавлении двух одинаковых товаров в корзине не будет новой записи, но исходный товар получит «количество» равное 2. Так что, но не тогда, когда они, например, разных размеров. Таким образом, каждый элемент идентифицируется уникальным rowId на основе его идентификатора и параметров.

Это код, который генерирует rowId (который используется методом add()):

protected function generateRowId(CartItem $item)
{
    return md5($item->getId() . serialize($item->getOptions()));
}

Теперь я написал свой тест следующим образом:

public function it_can_add_multiple_instances_of_a_cart_item(CartItem $cartItem1, CartItem $cartItem2)
{
    $this->add($cartItem1);
    $this->add($cartItem2);

    $this->shouldHaveCount(2);
}

Но проблема в том, что обе заглушки возвращают null для метода getId(). Итак, я попытался установить willReturn() для этого метода, поэтому мой тест стал таким:

public function it_can_add_multiple_instances_of_a_cart_item(CartItem $cartItem1, CartItem $cartItem2)
{
    $cartItem1->getId()->willReturn(1);
    $cartItem2->getId()->willReturn(2);

    $this->add($cartItem1);
    $this->add($cartItem2);

    $this->shouldHaveCount(2);
}

Но теперь я получаю ошибки, говорящие мне, что неожиданные методы вызываются как getName(). Поэтому я должен сделать то же самое для всех вызываемых методов интерфейса CartItem:

public function it_can_add_multiple_instances_of_a_cart_item(CartItem $cartItem1, CartItem $cartItem2)
{
    $cartItem1->getId()->willReturn(1);
    $cartItem1->getName()->willReturn(null);
    $cartItem1->getPrice()->willReturn(null);
    $cartItem1->getOptions()->willReturn([]);

    $cartItem2->getId()->willReturn(2);
    $cartItem2->getName()->willReturn(null);
    $cartItem2->getPrice()->willReturn(null);
    $cartItem2->getOptions()->willReturn([]);

    $this->add($cartItem1);
    $this->add($cartItem2);

    $this->shouldHaveCount(2);
}

Теперь это работает, тест зеленый. Но это кажется неправильным... Я что-то упустил или это ограничение PHPSpec?


person Crinsane    schedule 03.12.2014    source источник


Ответы (3)


Теперь это работает, тест зеленый. Но это кажется неправильным... Я что-то упустил или это ограничение PHPSpec?

Я думаю, это хорошо, что в этом случае это кажется неправильным, потому что так и должно быть. Как упоминалось выше @ l3l0, PHPSpec — это инструмент дизайна, и он дает вам четкое представление о вашем дизайне здесь.

С чем вы боретесь, так это с тем, что ваш Cart нарушает принцип единой ответственности — он делает больше, чем одну вещь — он управляет CartItems, а также знает, как генерировать из него RowId. Поскольку PHPSpec заставляет вас заглушить все поведение CartItem, это дает вам сообщение о необходимости реорганизации генерации RowId.

Теперь представьте, что вы извлекли RowIdGenerator в отдельный класс (с его собственными спецификациями, которые здесь не рассматриваются):

class RowIdGenerator
{
    public function fromCartItem(CartItem $item)
    {
        return md5($item->getId() . serialize($item->getOptions()));
    }
}

Затем вы вводите этот генератор через конструктор как зависимость от вашей корзины:

class Cart
{
    private $rowIdGenerator;

    public function __construct(RowIdGenerator $rowIdGenerator)
    {
        $this->rowIdGenerator = $rowIdGenerator;
    }
}

Тогда ваша окончательная спецификация может выглядеть так:

function let(RowIdGenerator $rowIdGenerator)
{
    $this->beConstructedWith($rowIdGenerator);
}

public function it_can_add_multiple_instances_of_a_cart_item(RowIdGenerator $rowIdGenerator, CartItem $cartItem1, CartItem $cartItem2)
{
    $rowIdGenerator->fromCartItem($cartItem1)->willReturn('abc');
    $rowIdGenerator->fromCartItem($cartItem1)->willReturn('def');

    $this->add($cartItem1);
    $this->add($cartItem2);

    $this->shouldHaveCount(2);
}

И поскольку вы издевались над поведением генератора идентификаторов (и вы знаете, что это общение должно произойти), теперь вы соответствуете SRP. Тебе сейчас лучше?

person Kacper Gunia    schedule 10.12.2014

Итак, вы идете в ресторан, чтобы поужинать. Вы ожидаете, что вам будет предоставлен выбор еды, из которой вы выберете то, что вам действительно интересно съесть сегодня, и будете платить за это в конце ночи. Чего вы не ожидаете, так это того, что ресторан также возьмет с вас плату за прекрасную пару рядом с вами, заказавшую бутылку за бутылкой Chteau Margaux 95. Поэтому, когда вы обнаружите, что вы были strong> взимают плату за их еду, вы, вероятно, захотите немедленно позвонить в этот ресторан и в свой банк, потому что это совершенно не нормально, что это произошло без вашего ожидания!

Вопрос не в том, почему PhpSpec заставляет вас заглушать методы, которые вам сейчас не нужны. Вопрос в том, почему вы вызываете методы, которые вам сейчас не нужны. Если они не входят в ваши ожидания, PhpSpec просто позвонит в ваш банк, потому что это совершенно не нормально, что это произошло без вашего ожидания!

person everzet    schedule 10.12.2014

Да, вы можете назвать это «ограничением» phpspec. По сути, phpspec — это строгий TDD и инструмент проектирования объектной связи IMO.

Вы видите, что добавление $cartItem в коллекцию делает гораздо больше, чем вы ожидаете.

Во-первых, вам не нужно использовать заглушки (если вам не нужна связь между внутренними объектами), пример:

function it_adds_multiple_instances_of_a_cart_item()
{
    $this->add(new CartItem($id = 1, $options = ['size' => 1]));
    $this->add(new CartItem($id = 2, $options = ['size' => 2]));

    $this->shouldHaveCount(2);
}

function it_adds_two_same_items_with_different_sizes()
{
    $this->add(new CartItem($id = 1, $options = ['size' => 1]));
    $this->add(new CartItem($id = 1, $options = ['size' => 2]));

    $this->shouldHaveCount(2);   
}

function it_does_not_add_same_items()
{
    $this->add(new CartItem($id = 1, $options = []));
    $this->add(new CartItem($id = 1, $options = []));

    $this->shouldHaveCount(1);   
}

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

function it_adds_multiple_instances_of_a_cart_item(CartItem $cartItem1, CartItem $cartItem2)
{
   $this->add($cartItem1);
   $cartItem1->isSameAs($cartItem2)->willReturn(false);
   $this->add($cartItem2);

   $this->shouldHaveCount(2);
}

function it_does_not_add_same_items((CartItem $cartItem1, CartItem $cartItem2)
{
    $this->add($cartItem1);
    $cartItem1->isSameAs($cartItem2)->willReturn(true);
    $this->add($cartItem2);

    $this->shouldHaveCount(1);   
}
person l3l0    schedule 07.12.2014
comment
Интересный ответ, я уже «боялся», что это ограничение. Меня действительно не очень интересует внутренняя связь объектов. Но причина, по которой мне нужны заглушки, заключается в том, что CartItem — это не класс, а интерфейс. Я хочу, чтобы люди могли использовать любой класс, который они хотят, до тех пор, пока они реализуют этот интерфейс. Поэтому я не могу просто создать экземпляр new CartItem(). - person Crinsane; 08.12.2014
comment
Иметь геттеры и сеттеры и/или сумматоры в одном интерфейсе - не очень крутая идея ;) Может быть, тогда можно упростить интерфейс и сделать меньше методов. Вас может заинтересовать принцип разделения интерфейсов. - person l3l0; 08.12.2014
comment
Как еще я мог бы это сделать, я имею в виду, все, что меня волнует, это то, что любой предмет, который кто-то пытается положить в корзину, имеет имя, которое я могу получить, идентификатор, который я могу получить, и т. д. Вот почему мне нужна эта горстка геттеров... Может быть, я неправильно понимаю, но для меня это имеет смысл. Мне все равно, какой это объект, пока я могу получить идентификатор, имя, цену и параметры. :) - person Crinsane; 10.12.2014