【问题标题】:Mock only one method on PHPSpec stubs在 PHPSpec 存根上仅模拟一种方法
【发布时间】:2014-12-03 21:38:01
【问题描述】:

好的,所以我正在尝试将我的一个包转移到 PHPSpec 测试,但很快我遇到了这个问题。 packages 是一个购物车包,所以我想测试一下,当您将两个商品添加到购物车时,购物车的计数为 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);
}

但问题是,对于 getId() 方法,两个存根都返回 null。所以我尝试为该方法设置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 的限制?

【问题讨论】:

    标签: php testing mocking phpspec stubs


    【解决方案1】:

    现在可以了,测试是绿色的。但是感觉不对……是我遗漏了什么还是这是对 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。你现在感觉好些了吗?

    【讨论】:

      【解决方案2】:

      所以你走进一家餐馆是为了吃晚饭。您希望您可以选择一顿饭,从中选择您今天真正感兴趣的一顿饭,并在晚上结束时收取费用。您没想到的是,餐厅还会向您旁边的一对可爱的夫妇收取一瓶又一瓶 Chteau Margaux 95 的费用。所以当您发现您是 strong> 他们的餐费也被收取了,您可能会想立即致电那家餐厅和您的银行,因为这完全不行这发生在您没有预料到的情况下!

      问题不在于为什么 PhpSpec 会强制你存根方法你现在不关心。问题是你为什么要调用你现在不关心的方法。如果它们不符合您的期望,PhpSpec 只会为您致电您的银行,因为这完全不行,他在您没有预料到的情况下发生了!

      【讨论】:

        【解决方案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);   
        }
        

        【讨论】:

        • 有趣的答案,我已经“害怕”这是一个限制。我确实对内部对象通信并不感兴趣。但我需要存根的原因是因为 CartItem 不是一个类,而是一个接口。我希望人们能够使用他们想要的任何类,只要他们实现了那个接口。所以我不能只实例化一个new CartItem()
        • 在一个接口中拥有 getter 和 setter 或/和 adders 并不是很酷的主意 ;) 也许你可以简化接口并减少方法。你可以对接口隔离原理感兴趣。
        • 我该怎么做呢,我的意思是,我关心的是,无论有人试图放入购物车中的任何物品,有一个我能得到的名字,一个我能得到的 ID,等等. 所以这就是为什么我需要这些少数吸气剂......也许我看错了,但对我来说这很有意义。我不在乎它是什么类型的对象,只要我能得到一个 ID、名称、价格和选项。 :)
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-10-15
        • 2014-09-15
        • 2013-01-28
        • 1970-01-01
        • 1970-01-01
        • 2011-09-25
        • 1970-01-01
        相关资源
        最近更新 更多