单元测试和对象继承


unit tests and object-inheritance

我有一个关于"单元测试"和对象继承的问题。例如:

我有一个类a,它继承了类b。让我们假设这两个类的唯一区别是add方法。在类B中,这个add方法稍微扩展了一下。现在我想为类B的add函数写一个单元测试,但由于parent::add调用,我依赖于父类a。在这种情况下,我不能模拟父类的add方法,所以结果测试将是一个集成测试,但如果我想它是一个单元测试?我不希望类B中添加方法的测试因为类a中的父方法而失败。在这种情况下,只有父方法的单元测试应该失败。

class B exends A
{
    public function add($item)
    {
        parent::add($item);
        //do some additional stuff  
    }
    ....
}
class A
{
    protected $items = [];
    public function add($item)
    {
        $this->items[] = $item;
    }
    ....
}

当然,我可以使用对象聚合并将父对象传递给子构造函数,因此我可以模拟父方法add,但这是最好的方法吗?我将很少再使用对象继承了。

class B
{
    protected $a;
    public function __contruct(A $a)
    {
        $this->a = $a;
    }
    public function add($item)
    {
        $this->a->add($item);
        //do some additional stuff  
    }
    ....
}
class A
{
    protected $items = [];
    public function add($item)
    {
        $this->items[] = $item;
    }
    ....
}
我将非常感谢你的意见。谢谢!

问问自己,你想要实现什么样的继承?如果B 是一种 a,那么你想要接口继承。如果B与a共享大量代码,那么您需要实现继承。有时两者都需要。

接口继承将语义划分为严格的层次结构,从泛化到专门化组织语义。认为分类。接口(方法签名)表示行为:类响应的消息集,以及类发送的消息集。当从一个类继承时,您隐式地接受父类代表您发送的所有消息的责任,而不仅仅是它可以接收的消息。由于这个原因,超类和子类之间的耦合是紧密的,每个类都必须严格替代另一个类(参见Liskov替代原则)。

实现继承将数据表示和行为(属性和方法)的机制封装到一个方便的包中,以便子类重用和增强。根据定义,子类继承父类的接口,即使它只需要实现。

最后一部分很关键。再读一遍:子类继承接口,即使他们只想要实现。

B是否严格要求A的接口?B能代替A吗,在所有情况下都匹配协方差和反方差?

  • 如果答案是,那么你有真正的子类型。祝贺你。现在你必须测试相同的行为两次,因为在B中你负责维护A的行为:对于A能做的每件事,B必须能够做到。

  • 如果答案是,那么您只需要共享实现,测试实现是否工作,然后测试B和A分别分派到实现中。

在实际中,我避免使用extends。当我想要实现继承时,我使用trait在一个地方定义static行为,然后使用use在需要的地方合并它。当我想要接口继承时,我定义了许多狭窄的interface,然后在所有具体类型中与implements结合,可能使用trait来利用行为。

对于你的例子,我会这样做:

trait Container {
    public function add($item) { $this->items[] = $item; }
    public function getItems() { return $this->items; }
    private $items = [];
}
interface Containable { public function add($item); }
class A implements Containable { use Container; }
class B implements Containable {
    use Container { Container::add as c_add; }
    public function add($item) {
        $this->c_add($item);
        $this->mutate($item);
    }
    public function mutate($item) { /* custom stuff */ }
}

Container::addB::mutate将进行单元测试,而B::add将进行集成测试。

总之,更倾向于组合,因为extends是邪恶的。阅读ThoughtWorks入门文章《组合与继承:如何选择》,以获得对设计权衡的更深入理解。

"静态行为",你问?是的。低耦合是一个目标,这也适用于特性。尽可能地,trait应该只引用它定义的变量。最安全的方法是使用静态方法,将它们的所有输入作为形式参数。最简单的方法是在trait中定义成员变量。(但是,请避免让trait使用在trait中没有明确定义的成员变量——否则,这就是盲目耦合!)我发现,性状成员变量的问题是,当混合多个性状时,会增加碰撞的机会。不可否认,这很小,但对于库作者来说,这是一个实用的考虑。