从抽象 class 测试 child-class 的抽象方法

Testing an abstract method of a child-class from an abstract class

为了继续使用我使用的相同示例 here:

我现在想测试 child-classes 中受保护方法的实现。
因为我在我的抽象测试中存根它们 class,实现本身没有被测试。
但是 protected-method 没有正常测试,所以我希望你能提供关于如何测试它们的建议。

就像my other thread一样,我想解决这个问题而不重构我的代码

Parent-class:

abstract class Order
{
    public function __construct( $orderId, User $user )
    {
        $this->id = $this->findOrderId( $user->getId(), $orderId );

        if ($this->id !== false) {
            $this->setOrderData();
        }
    }

    abstract protected function findOrderId( $userId, $orderIdToSearch );

    private function setOrderData()
    {
        ...
    }
}

Child-class 测试:

public class OrderTypeA extends Order
{
    protected function findOrderId($userId, $orderId)
    {
        ...
    }
}

测试代码:

class OrderTypeATest extends PHPUnit_Framework_TestCase
{
    public function testFindOrderId() {
        ???
    }
}

如果您只有在找到正确的订单后才能获得有效的 $this->id。做一些像:

$order = new OrderTypeA($orderId, $user);
$this->assertNotEquals(false,$order->id);

或者如果 $orderId 等于 $this->id

$order = new OrderTypeA($orderId, $user);
$this->assertEquals($orderId,$order->id);

但此处显示的 code/logic 不足以告诉您更多信息;)

你的抽象对我来说没有意义。

我了解到您有一个代表订单的对象。您通过提供用户和订单 ID 来实例化它。然而订单的类型不止一种,这些类型的订单之间的区别在于您在数据库存储中搜索它们的方式?听起来不对。

您的代码确实讲述了一个奇怪的故事。您有这个订单 ID,您要做的第一件事就是搜索订单 ID。我只是认为您已经拥有订单 ID,因此不需要再次搜索它。或者该方法的名称可能错误,而不是 findOrderId() 它应该被称为 findOrderById() - 或 findUserOrderById().

此外,您确实在构造函数中工作。搜索东西不应该在那里完成。

您的测试问题来自于您决定将不同的搜索策略实现为抽象方法这一事实。您必须测试一个受保护的抽象方法,这并不容易。这也使得 属性 测试主要抽象顺序 class 变得困难,因为你必须提供一个实现 - 这个实现听起来像是隐藏了一个数据库访问层,所以可能会出现很多问题在真实代码中。

我建议不要让订单自行搜索。搜索订单应该在订单对象之外完成。这样,您很可能将该搜索实现为 public 方法,可以对其进行正常测试。搜索订单的代码将决定您是否成功找到了 OrderTypeA,或者可能缺少 MissingOrderTypeA,两者都扩展了订单 class。订单对象应该携带订单数据,而不是在数据库中查找它们的搜索逻辑。

提示:如果您在测试代码时遇到问题,99.9% 的可能性是您的代码试图以错误的方式做事。这并不是说事情不能那样做,而是说您将要生成难以测试的代码,也难以维护,寻找替代策略来实施解决方案是个好主意。优雅的代码总是易于测试,因为所有必要的方法都在相关 class 中 public,因此可以按预期一起工作。

您可以使用反射测试 protected/private 方法。阅读此 tutorial。在其他解决方案中,您会发现直接解决方案:

/**
 * Call protected/private method of a class.
 *
 * @param object &$object    Instantiated object that we will run method on.
 * @param string $methodName Method name to call
 * @param array  $parameters Array of parameters to pass into method.
 *
 * @return mixed Method return.
 */
 public function invokeMethod(&$object, $methodName, array $parameters = array())
 {
     $reflection = new \ReflectionClass(get_class($object));
     $method = $reflection->getMethod($methodName);
     $method->setAccessible(true);

     return $method->invokeArgs($object, $parameters);
 }

另外,关于你之前的问题,你在哪试的摘要class。带有 phpunit 模拟的解决方案必须有效。但是如果你用PHP7,你可以用Anonymous classes达到同样的效果:

abstract class Order
{
    protected $id;

    public function __construct($orderId, $userId)
    {
        $this->id = $this->findOrderId($userId, $orderId);

        if ($this->id !== false) {
            $this->setOrderData();
        }
    }

    abstract protected function findOrderId($userId, $orderIdToSearch);

    private function setOrderData()
    {
        echo 'setOrderData';
    }
}

$orderId = 1;
$userId = 1;

$order = new class($orderId, $userId) extends Order {
    protected function findOrderId($userId, $orderIdToSearch)
    {
        return 1;
    }
};

您最终会得到可用的 $order 对象,它已准备好进行测试。将此代码放在测试用例的 setUp() 方法中也是个好主意。