如何对抽象类进行单元测试:使用存根进行扩展?

518

我在思考如何对抽象类和继承自抽象类的类进行单元测试。

我应该通过继承抽象类,将其抽象方法替换为存根(stub),然后测试所有具体方法吗?然后只测试我覆盖的方法,并在继承我的抽象类的对象的单元测试中测试抽象方法?

我是否应该有一个抽象的测试用例来测试抽象类的方法,并在继承抽象类的对象的测试用例中扩展此类?

请注意,我的抽象类具有一些具体方法。


2
最好不要直接对抽象类进行单元测试:https://enterprisecraftsmanship.com/posts/how-to-unit-test-an-abstract-class/ - Vladimir
1
如果你有20或30个方法在所有子类中都有完全相同的实现,那该怎么办?你仍然应该在所有方法中复制测试吗?这样做没有太多意义。 - Farid
14个回答

521

抽象基类有两种使用方式:

  1. 你正在专门化你的抽象对象,但是所有客户端都将通过其基本接口使用派生类。

  2. 您正在使用抽象基类来消除设计中对象中的重复,并且客户端通过它们自己的接口使用具体实现。!


解决方案1 - 策略模式

Option1

如果您遇到第一种情况,则实际上有一个接口由抽象类中的虚拟方法定义,您的派生类正在实现该接口。

您应该考虑使其成为真正的接口,将抽象类更改为具体类,并在其构造函数中获取此接口的实例。然后,您的派生类变为这个新接口的实现。

IMotor

这意味着您现在可以使用新接口的模拟实例测试以前的抽象类,每个新实现都通过现在的公共接口。一切都很简单和可测试。


解决方案2

如果您遇到第二种情况,则您的抽象类作为辅助类起作用。

AbstractHelper

查看它包含的功能。看看是否可以将其中任何内容推送到正在操作的对象上,以最小化这种重复。如果仍有剩余内容,请考虑将其作为辅助类,使具体实现在其构造函数中接受并删除其基类。

Motor Helper

这再次导致简单且易于测试的具体类。


作为规则

复杂的简单对象网络优于复杂对象的简单网络。

可扩展可测试代码的关键是小型构建块和独立连接。


更新:如何处理两者的混合?

有可能存在一个基类同时执行这两个角色...即它有一个公共接口,并且具有受保护的辅助方法。如果是这种情况,那么你可以将辅助方法提取到一个类中(方案2),并将继承树转换为策略模式。

如果你发现基类实现了一些直接的方法并且其他方法是虚拟的,那么你仍然可以将继承树转换为策略模式,但我认为这也表明责任没有正确对齐,可能需要重构。


更新2:作为一个过渡步骤的抽象类(2014/06/12)

我最近遇到了这样一种情况,我使用了抽象类,所以我想探讨一下原因。

我们有一个标准格式用于配置文件。这个特定的工具有3个按照该格式排布的配置文件。我想要为每个设置文件创建一个强类型类,这样通过依赖注入,一个类就可以请求它关心的设置。

我通过创建一个抽象基类来实现这个目标,该基类知道如何解析设置文件格式,派生类公开相同的方法,但是封装了设置文件的位置。

我本可以写一个"SettingsFileParser",由3个类进行包装,然后通过基类委托来公开数据访问方法。但我之所以没有这样做,是因为它会导致3个派生类中有更多的委托代码而不是其他任何东西。

然而...随着这段代码的发展和每个这些设置类的消费者变得更加清晰。每个设置用户将请求一些设置并以某种方式转换它们(由于设置是文本,它们可能将它们包装在对象中或将它们转换为数字等)。随着这一点的发生,我将开始将此逻辑提取到数据操作方法中,并将其推回到强类型设置类中。这将导致每组设置的更高级别接口,最终不再知道它正在处理“设置”。

此时,强类型设置类将不再需要公开底层“设置”实现的“getter”方法。

此时,我不再希望他们的公共接口包括设置访问器方法;因此,我将更改此类,以封装一个设置解析器类,而不是从它继承。

抽象类因此是:我在这一刻避免委托代码的一种方式,并在代码中标记提醒我稍后更改设计。也许我永远不会做到这一点,所以它可能存在很长时间...只有代码能告诉我们。

我发现这对于任何规则都是正确的,比如“没有静态方法”或“没有私有方法”。它们指示代码中的问题...这是好的。它让你寻找你错过的抽象...并在此期间继续为客户提供价值。

我想象这样的规则定义了一个景观,在那里可维护的代码位于山谷中。当添加新行为时,就像雨滴落在您的代码上。最初,您将其放在任何地方...然后进行重构,以允许良好设计的力量推动行为周围,直到所有内容都在山谷中结束。


21
这是一个很棒的回答,比排名第一的回答好多了。不过我想只有那些真正想编写可测试代码的人才会欣赏它.. :) - MalcomTucker
25
我无法忘记这个回答有多好。它彻底改变了我对抽象类的思考方式。谢谢Nigel。 - MalcomTucker
4
哦不...又有一个原则我需要重新思考!谢谢(现在是讽刺的,等我吸收了并感觉成为更好的程序员时就不再是讽刺的了) - Martin Lyne
12
好的回答。这绝对是值得思考的事情... 但是你所说的基本上归结为不要使用抽象类,对吗? - brianestey
38
仅对这个规则点个赞,即“将简单对象的复杂网络优先于复杂对象的简单网络”。 - David Glass
显示剩余25条评论

286

编写一个Mock对象并仅用于测试。它们通常非常非常非常简单(继承自抽象类),而且不会更多。然后,在您的单元测试中,您可以调用要测试的抽象方法。

您应该测试包含一些逻辑的抽象类,就像您拥有的所有其他类一样。


11
该死,我不得不说这是我第一次同意使用模拟的想法。 - Jonathan Allen
6
你需要两个类,一个是 Mock 类和一个是 Test 类。Mock 类仅扩展要测试的抽象类中的抽象方法。这些方法可以是 no-op、返回 null 等,因为它们不会被测试。Test 类则仅测试非抽象公共 API(即由抽象类实现的接口)。对于任何继承抽象类的类,你都需要额外的测试类,因为抽象方法没有得到覆盖。 - cyber-monk
10
显然可以这样做。但是,为了真正测试任何派生类,您需要一遍又一遍地测试这个基本功能。这将导致代码重复,因此建议使用一个抽象测试夹具来消除这种重复。这些都有点不妥。我强烈建议重新审视首先使用抽象类的原因,并查看是否有更好的解决方法。 - Nigel Thorne
@Nigel Thorne,不是说你错了什么的,只是我对你的评论感到困惑。话虽如此,如果我在你的设计中使用抽象类来消除对象中的重复,并且客户端通过自己的接口使用具体实现,那么这样的设计有什么问题吗?在我的使用中,我有大约20个类扩展了我的抽象类,它具有所有扩展类都使用的具体方法。这段代码有什么问题吗?有什么更好的方法来做到这一点? - liltitus27
2
@liltitus27,有很多种编写代码的方式可以让编译器理解。作为一名经验丰富的程序员,您已经过滤了可能设计中高耦合、混乱或难以维护的设计集合。通过您学到的过滤器的设计被归类为“好的设计”。我还有一个评判设计的标准。它是否容易测试? - Nigel Thorne
显示剩余6条评论

12

我的抽象类和接口处理方式如下:我编写一个测试,将对象用作具体实现。但是类型为X(X是抽象类)的变量在测试中未设置。这个测试类不会被添加到测试套件中,但其子类具有设置方法,可将该变量设置为X的具体实现。这样我就不会重复测试代码。未使用测试的子类可以添加更多测试方法。


这难道不会在子类中引起类型转换问题吗?如果 X 有一个方法 a,而 Y 继承了 X,但也有一个方法 b。当你对测试类进行子类化时,难道不必将抽象变量转换为 Y 类型,以对 b 进行测试吗? - Johnno Nolan
这会重复测试工作,因为您的测试会测试位于基类中的逻辑。 - user625488

11

如果要对抽象类进行单元测试,您应该派生它以进行测试目的,测试基类.method()的结果以及继承时预期的行为。

通过实现来测试抽象类,就像测试方法一样调用它...


9
如果您的抽象类包含具有业务价值的具体功能,则我通常会直接测试它,方法是创建一个测试替身来存根抽象数据,或者使用模拟框架为我完成这项工作。我选择哪个取决于是否需要编写针对抽象方法的测试特定实现。

我需要这样做的最常见情况是当我使用模板方法模式时,例如当我正在构建某种可扩展框架,该框架将由第三方使用。在这种情况下,抽象类是定义要测试的算法的内容,因此更有意义的是测试抽象基类而不是特定实现。

但是,我认为这些测试应该专注于真正的业务逻辑的具体实现;您不应该单元测试抽象类的实现细节,因为这会导致脆弱的测试。


6
一种方法是编写一个对应于抽象类的抽象测试用例,然后编写继承该抽象测试用例的具体测试用例。对于原始抽象类的每个具体子类都要这样做(即,测试用例层次结构与类层次结构相同)。请参见《JUnit Recipes》书中的“在JUnit中测试接口”章节:http://safari.informit.com/9781932394238/ch02lev1sec6https://www.manning.com/books/junit-recipeshttps://www.amazon.com/JUnit-Recipes-Practical-Methods-Programmer/dp/1932394230 如果您没有Safari账户。

还可以参见xUnit patterns中的Testcase Superclass:http://xunitpatterns.com/Testcase%20Superclass.html


4

当我设置测试抽象类的测试框架时,通常会遵循以下模式:

public abstract class MyBase{
  /*...*/
  public abstract void VoidMethod(object param1);
  public abstract object MethodWithReturn(object param1);
  /*,,,*/
}

我正在测试的版本是:

public class MyBaseHarness : MyBase{
  /*...*/
  public Action<object> VoidMethodFunction;
  public override void VoidMethod(object param1){
    VoidMethodFunction(param1);
  }
  public Func<object, object> MethodWithReturnFunction;
  public override object MethodWithReturn(object param1){
    return MethodWihtReturnFunction(param1);
  }
  /*,,,*/
}

如果在我不期望的时候调用抽象方法,测试就会失败。在安排测试时,我可以轻松地使用执行断言、抛出异常、返回不同值等的lambda表达式来存根抽象方法。


4
我认为“抽象”测试有问题。我认为测试是一个具体的想法,没有抽象概念。如果您有共同的元素,请将它们放入帮助程序方法或类中供所有人使用。
至于测试抽象测试类,请确保问自己正在测试什么。有几种方法,您应该找出在您的情况下哪种方法有效。您是否尝试在子类中测试新方法?那么只让您的测试与该方法交互。您是否在测试基类中的方法?然后可能需要为该类单独创建一个夹具,并使用尽可能多的测试来逐个测试每个方法。

我不想重新测试已经测试过的代码,这就是为什么我选择了抽象测试用例的方法。我正试图在一个地方测试我抽象类中的所有具体方法。 - Paul Whelan
7
我不同意将常用元素提取到辅助类中,至少在某些(许多?)情况下是这样的。如果抽象类包含一些具体功能,我认为直接对该功能进行单元测试是完全可以接受的。 - Seth Petry-Johnson
如果您有数十个具体实现的方法,那么只需将其复制粘贴到所有具体实现中吗?这毫无意义。 - Farid

3
如果具体方法调用了任何策略中的抽象方法,那么该策略就不起作用了,您需要单独测试每个子类的行为。否则,按照您所描述的方式扩展它并存根抽象方法应该是可以的,只要抽象类的具体方法与子类解耦即可。

2

首先,如果抽象类包含一些具体方法,我认为你应该考虑以下示例:

 public abstract class A 

 {

    public boolean method 1
    {
        // concrete method which we have to test.

    }


 }


 class B extends class A

 {

      @override
      public boolean method 1
      {
        // override same method as above.

      }


 } 


  class Test_A 

  {

    private static B b;  // reference object of the class B

    @Before
    public void init()

      {

      b = new B ();    

      }

     @Test
     public void Test_method 1

       {

       b.method 1; // use some assertion statements.

       }

   }

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