好的,所以我正在尝试将我的一个包移动到 PHPSpec 测试中,但很快我就遇到了这个问题。包裹是购物车包,所以我想测试一下,当您向购物车添加两件商品时,购物车的计数为两,很简单。但是,当然,在购物车中,当添加两个相同的项目时,购物车中不会有新条目,但原始项目将获得"数量"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 的限制?
现在这行得通,测试是绿色的。但感觉不对劲...我错过了什么还是这是对 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);
}
而且因为你嘲笑了id生成器的行为(你知道这种通信必须发生(,现在你符合SRP。你现在感觉好些了吗?
所以你走进一家餐馆是为了吃晚饭。你希望你能选择一顿饭,从中选择你今天真正感兴趣的一顿饭,并在晚上结束时被收取费用。您不希望的是,餐厅还会向您旁边的可爱夫妇收取费用,订购一瓶又一瓶的玛歌酒庄95。因此,当您发现您也被收取了他们的餐费时,您可能会想立即致电该餐厅和您的银行,因为这完全不行,这在您没有预料到的情况下发生!
问题不在于为什么 PhpSpec 强迫你存根你现在不关心的方法。问题是你为什么称你现在不关心的方法。如果它们不是您期望的一部分,PhpSpec 只会为您致电您的银行,因为这完全不行,他在您意想不到的情况下发生!
是的,你可以称之为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);
}