单元测试和对象继承
unit tests and object-inheritance
我有一个关于 "unit tests" 和对象继承的问题。例如:
我有一个扩展 class B 的 class A。我们假设这两个 classes 的唯一区别是添加方法。在 class B 中,此添加方法略有扩展。现在我想为 class B 的添加函数编写一个单元测试,但是由于 parent::add 调用我对父 class A 有依赖性。在这种情况下我不能模拟父 class 的添加方法,因此生成的测试将是一个集成测试,但如果我希望它成为一个单元测试?我不希望 class B 中的方法添加测试失败,因为 class 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;
}
....
}
我当然可以使用对象聚合并将我的父对象传递给子构造函数,因此我可以模拟父方法添加,但这是最好的方法吗?我很少再使用对象继承了。
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 共享大量代码,那么您需要 实现继承。有时你两者都想要。
接口继承class将语义意义划分为严格的层次结构,该意义从广义到专门组织。想想taxonomy. The interface (method signatures) represent the behaviors: both the set of messages to which the class responds, as well as the set of messages that the class sends. When inheriting from a class, you implicitly accept responsibility for all messages the superclass sends on your behalf, not just the messages that it can receive. For this reason, the coupling between super- and sub-class is tight and each must strictly substitute for the other (see Liskov Substitution Principle).
实现继承将数据表示和行为(属性和方法)的机制封装到一个方便的包中,以供子classes 重用和增强。根据定义,子 class 继承父接口,即使它只想要实现。
最后一部分很关键。再读一遍:Sub-classes继承接口,即使他们只想要实现。
B是否严格要求A的接口? B substitute 对于 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::add
和 B::mutate
将进行单元测试,而 B::add
将进行集成测试。
总而言之,更喜欢组合而不是继承,因为extends
is evil. Read the ThoughtWorks primer Composition vs. Inheritance: How To Choose可以更深入地了解设计权衡。
† "static behaviors",你问?是的。低耦合是一个目标,这适用于特征。一个特征应该尽可能只引用它定义的变量。 最安全 的实施方式是使用将所有输入作为正式参数的静态方法。 最简单的方法是在trait中定义成员变量。 (但是,请避免让特征使用特征中未明确定义的成员变量——否则,那是盲目耦合!)我发现,特征成员变量的问题是,当混合多个特征时,你增加了碰撞。诚然,这很小,但对于图书馆作者来说,这是一个实际的考虑。
我有一个关于 "unit tests" 和对象继承的问题。例如:
我有一个扩展 class B 的 class A。我们假设这两个 classes 的唯一区别是添加方法。在 class B 中,此添加方法略有扩展。现在我想为 class B 的添加函数编写一个单元测试,但是由于 parent::add 调用我对父 class A 有依赖性。在这种情况下我不能模拟父 class 的添加方法,因此生成的测试将是一个集成测试,但如果我希望它成为一个单元测试?我不希望 class B 中的方法添加测试失败,因为 class 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;
}
....
}
我当然可以使用对象聚合并将我的父对象传递给子构造函数,因此我可以模拟父方法添加,但这是最好的方法吗?我很少再使用对象继承了。
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 共享大量代码,那么您需要 实现继承。有时你两者都想要。
接口继承class将语义意义划分为严格的层次结构,该意义从广义到专门组织。想想taxonomy. The interface (method signatures) represent the behaviors: both the set of messages to which the class responds, as well as the set of messages that the class sends. When inheriting from a class, you implicitly accept responsibility for all messages the superclass sends on your behalf, not just the messages that it can receive. For this reason, the coupling between super- and sub-class is tight and each must strictly substitute for the other (see Liskov Substitution Principle).
实现继承将数据表示和行为(属性和方法)的机制封装到一个方便的包中,以供子classes 重用和增强。根据定义,子 class 继承父接口,即使它只想要实现。
最后一部分很关键。再读一遍:Sub-classes继承接口,即使他们只想要实现。
B是否严格要求A的接口? B substitute 对于 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::add
和 B::mutate
将进行单元测试,而 B::add
将进行集成测试。
总而言之,更喜欢组合而不是继承,因为extends
is evil. Read the ThoughtWorks primer Composition vs. Inheritance: How To Choose可以更深入地了解设计权衡。
† "static behaviors",你问?是的。低耦合是一个目标,这适用于特征。一个特征应该尽可能只引用它定义的变量。 最安全 的实施方式是使用将所有输入作为正式参数的静态方法。 最简单的方法是在trait中定义成员变量。 (但是,请避免让特征使用特征中未明确定义的成员变量——否则,那是盲目耦合!)我发现,特征成员变量的问题是,当混合多个特征时,你增加了碰撞。诚然,这很小,但对于图书馆作者来说,这是一个实际的考虑。