在PHPSpec存根上只模拟一种方法

时间:2014-12-03 21:38:01

标签: php testing mocking phpspec stubs

好的,我正试图将我的一个软件包移到PHPSpec测试中,但很快就遇到了这个问题。 这些包是一个购物车包,所以我想测试一下,当你向购物车添加两个商品时,购物车的数量为2,简单。 但是当然,在购物车中,当添加两个相同的项目时,购物车中不会有新的条目,但原始项目将获得2的'数量'。但是,例如,当它们是不同的尺寸。 因此,每个项目都由唯一的rowId标识,基于它的ID和选项。

这是生成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的限制吗?

3 个答案:

答案 0 :(得分:9)

  

现在这个工作,测试是绿色的。但感觉不对......我错过了什么或者这是对PHPSpec的限制吗?

我认为在这种情况下感觉不错,因为它应该。正如上面提到的@ l3l0,PHPSpec是一个设计工具,它为您提供了有关您的设计的明确信息。

您挣扎的事实是,Cart违反了单一责任原则 - 它不止一件事 - 它管理CartItems并知道如何从中生成RowId。由于PHPSpec强制您保留CartItem的整个行为,因此它会为您提供重构生成RowId的消息。

现在假设您将RowIdGenerator提取为单独的类(此处未包含它自己的规范):

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

然后通过构造函数将此生成器作为依赖项注入到Cart:

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);
}

因为你嘲笑了id生成器的行为(并且你知道这种通信必须发生),所以现在你遵守了SRP。你现在觉得好些吗?

答案 1 :(得分:4)

所以你要走进餐馆才能吃晚饭。您希望您可以选择用餐,从中您可以选择您今天真正感兴趣的餐点,并在晚上收取费用。您不能期待的是,餐厅将向您收取您旁边订购一瓶又一瓶的Chateau Margaux 95的可爱情侣。所以当您确实发现您是对他们的用餐收费,你可能会立即打电话给那家餐馆和你的银行,因为完全不行这种情况发生在你没想到的情况下!

问题不在于为什么PhpSpec强迫你使用你现在不关心的方法。问题是为什么你打电话你现在不关心的方法。如果他们不符合您的期望,PhpSpec会为您打电话给您的银行,因为完全不正常他的发生在没有您期待的情况下发生!

答案 2 :(得分:3)

是的,你可以称之为"限制" 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);   
}