TDD和继承

4

我正在使用TDD开展我的第一个项目,但在继承方面遇到了困难。

例如,如果我有如下代码:

public interface IComponent
{
    void MethodA();

    void MethodB();
}

public class Component : IComponent
{
    public virtual void MethodA()
    {
        // Do something
    }

    public virtual void MethodB()
    {
        // Do something
    }
}

public class ExtendedComponent : Component
{
    public override void MethodA()
    {
        base.MethodA();

        // Do something else
    }
}

如果我不依赖于 Component,那么我就无法单独测试 ExtendedComponent。

然而,如果我使用组合来创建 ExtendedComponent,就像这样

public class ExtendedComponent : IComponent
{
    private readonly IComponent _component;

    public ComponentB(IComponent component)
    {
        _component = component;
    }

    public virtual void MethodA()
    {
        _component.MethodA();

        // Do something else
    }

    public virtual void MethodB()
    {
        _component.MethodB();
    }
}

我现在可以通过模拟包装的IComponent来单独测试ExtendedComponent。
这种方法的缺点是,如果我想添加新的方法到IComponent,则必须将新方法添加到Component、ExtendedComponent和任何其他实现中。使用继承,我只需将新方法添加到基础组件中,而不会破坏其他任何内容。
我真的希望能够进行清晰的测试,所以我更倾向于使用组合路线,但我担心仅仅为了进行单元测试而始终使用组合并不是一个有效的理由。此外,在基本层面上添加功能将需要创建大量繁琐的委托方法。
我非常希望得到一些关于其他人如何解决这类问题的建议。

2
我猜你对“隔离”测试的概念有些误解。它意味着在没有外部组件依赖的情况下测试或组件。然而,在继承的情况下,基类并不是外部于你的范围之外的。它实际上是你组件的一部分。 - undefined
4个回答

2

你不需要担心在孤立环境中测试扩展组件,因为它不依赖于组件,它本身就是一个组件(至少在你编写的方式中是这样)。

你最初为组件类编写的所有测试仍然有效,并测试扩展类中所有未更改的行为。你只需要编写新的测试来测试扩展组件中添加的功能。

public class ComponentTests{
  [Fact]
  public void MethodADoesSomething(){
    Assert.xxx(new Component().MethodA());//Tests the component class
  }

  [Fact]
  public void MethodBDoesSomething(){
    Assert.xxx(new Component().MethodB());//Tests the component class
  }
}

public class ExtendedComponentTests{
  [Fact]
  public void MethodADoesSomething(){
    Assert.xxx(new ExtendedComponent().MethodA());//Tests the extended component class
  }
}

你可以从上面看到,MethodA的功能被测试了组件和扩展组件两个方面。而新功能仅针对扩展组件进行测试。

2

这里的关键思想是在单元测试方面也可以有继承。在这种情况下,我采用以下方法。我会有一个平行的单元测试用例继承层次结构。例如:

[TestClass]
public abstract class IComponentTest
{
    [TestMethod]
    public void TestMethodA()
    {
         // Interface level expectations.
    }
    [TestMethod]
    public void TestMethodB()
    {
         // Interface level expectations.
    }
}

[TestClass]
public class ComponentTest : IComponentTest
{
    [TestMethod]
    public void TestMethodACustom()
    {
        // Test Component specific test, not general test
    }
    [TestMethod]    
    public void TestMethodBCustom()
    {
        // Test Component specific test, not general test
    }
}
[TestClass]
public class ExtendedComponent : ComponentTest 
{
    public void TestMethodACustom2()
    {
        // Test Component specific test, not general test
    }
}

每个抽象的测试类或具体的类都处理自己级别的期望。因此具有可扩展性和可维护性。

在提到测试夹具继承时加1分。这通常是一种优雅的单元测试子类层次结构的方法。然而,根据我的经验,它需要在测试类中拥有一个SUT属性,在每个测试类中的相关级别进行实例化,但您的示例中没有显示出来。(我想知道您的IComponentTest方法的主体是什么,您选择哪个IComponent的实现并在那里新建?) - guillaume31

2

使用组合的方法实现继承在大多数编译器中都是常见的做法,因此你得不偿失(需要大量样板代码)。所以当存在is-a关系时,请坚持使用继承;而当存在has-a关系时,请使用组合(当然这些并不是铁律)。


0
你说得对 - 在不适当的情况下使用组合而非继承是不可取的。基于您在此提供的信息,目前尚不清楚哪种方式更好。请问在这种情况下哪个更适合。如果使用继承,您将获得方法的多态性和虚拟化。如果使用组合,则有效地将“前端”逻辑与隔离的“后端”逻辑分开--这种方法更容易,因为更改底层组件不会像继承一样对其余代码产生连锁反应。
总之,这不应影响您测试代码的方式。有许多可用于测试的框架,但这不应影响您选择的设计模式。

你的意思是它不会影响测试的能力。代码的测试方式取决于它的编码方式。 - Arturo Hernandez

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接