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


446

我想知道如何对抽象类以及扩展抽象类的单元进行单元测试。

我是否应该通过扩展抽象类,对抽象方法进行存根测试,然后测试所有具体方法来测试抽象类?然后只测试覆盖的方法,并在单元测试中测试扩展我的抽象类的对象的抽象方法?

我是否应该有一个抽象测试用例,可用于测试抽象类的方法,并在我的测试用例中为扩展抽象类的对象扩展该类?

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

Answers:


268

编写一个Mock对象,并将其仅用于测试。它们通常非常非常非常小(从abstract类继承),而没有更多。然后,在单元测试中,您可以调用要测试的抽象方法。

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


9
该死,我不得不说这是我第一次同意使用模拟的想法。
乔纳森·艾伦,

5
您需要两个类,一个模拟和一个测试。模拟类仅扩展了受测抽象类的抽象方法。这些方法可以是no-op,返回null等,因为它们不会被测试。测试类仅测试非抽象公共API(即,由Abstract类实现的接口)。对于任何扩展Abstract类的类,您都需要其他测试类,因为未涵盖abstract方法。
网络僧侣

10
显然可以这样做。但是要真正测试任何派生类,您将要一遍又一遍地测试此基本功能。这将导致您拥有一个抽象的测试夹具,因此您可以在测试中排除这种重复。这都闻起来!我强烈建议您先看看为什么要使用抽象类,然后再看看其他方法是否会更好。
奈杰尔·索恩

5
过度评价的下一个答案要好得多。
马丁·斯帕默

22
@MartiSpamer:我不会说这个答案被高估了,因为它的撰写时间比您认为下面的答案要早得多(2年)。让我们鼓励一下Patrick,因为在他发布此答案时,它很棒。让我们互相鼓励。干杯
Marvin Thobejane 2013年

449

有两种使用抽象基类的方式。

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

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


1的解决方案-策略模式

选项1

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

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

电机

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


解决方案2

如果您遇到第二种情况,则您的抽象类将作为帮助程序类工作。

抽象助手

看一下它包含的功能。查看是否可以将其中任何一个推入正在操作的对象上以最大程度地减少此重复。如果还剩下什么,可以考虑将其作为帮助程序类,具体实现将这些帮助程序类纳入其构造函数并删除其基类。

电机助手

这又导致了简单且易于测试的具体类。


通常

在复杂对象的简单网络上偏爱简单对象的复杂网络。

可扩展的可测试代码的关键是小的构建块和独立的布线。


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

可以让一个基类同时扮演这两个角色……即:它具有公共接口,并具有受保护的帮助程序方法。如果是这种情况,那么您可以将辅助方法分解为一个类(方案2),并将继承树转换为策略模式。

如果您发现您的基类有直接实现的某些方法而其他方法是虚拟的,那么您仍然可以将继承树转换为策略模式,但是我也将其作为很好的指示,说明职责未正确对齐,并且可能需要重构。


更新2:抽象类作为垫脚石(2014/06/12)

前几天我遇到一种情况,我使用摘要,所以我想探究原因。

我们的配置文件具有标准格式。此特定工具具有3个配置文件,所有配置文件均采用该格式。我希望为每个设置文件提供一个强类型的类,因此,通过依赖注入,一个类可以要求它关心的设置。

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

我本可以编写3个类包装的“ SettingsFileParser”,然后委托给基类以公开数据访问方法。我选择不这样做因为这将导致3派生类更代表团在他们的代码比什么都重要。

但是,随着代码的发展,这些设置类的使用者变得更加清晰。每个设置用户都会要求一些设置并以某种方式进行转换(由于设置是文本,因此他们可以将它们包装在将其转换为数字等的对象中)。发生这种情况时,我将开始将此逻辑提取到数据操作方法中,并将它们推回到强类型设置类中。这将导致每组设置都有一个更高级别的界面,最终不再意识到它正在处理“设置”。

此时,强类型设置类将不再需要用于公开基础“设置”实现的“获取”方法。

到那时,我不再希望它们的公共接口包含设置访问器方法。因此,我将更改此类以封装设置解析器类,而不是从其派生。

因此,Abstract类是:一种让我现在避免委托代码的方法,以及一种代码中的标记来提醒我以后更改设计。我可能永远也做不到,所以它可能会生存很长一段时间……只有代码能说明问题。

我发现这适用于任何规则,例如“没有静态方法”或“没有私有方法”。它们在代码中指示出一种气味...这很好。它使您始终寻找丢失的抽象……并使您在此期间继续为客户提供价值。

我想象像这样的规则定义了一个格局,可维护的代码生活在山谷中。当您添加新行为时,就像代码上的雨水一样。最初,您将其放置在其着陆的任何位置..然后,您进行重构以允许良好设计的力量将行为推向四周,直到一切最终落入谷底。


18
这是一个很好的答案。比最高评价要好得多。但是后来我猜只有那些真正想编写可测试代码的人会喜欢它.. :)
MalcomTucker 2012年

22
我无法克服这个答案到底有多好。这完全改变了我对抽象类的思考方式。谢谢奈杰尔。
MalcomTucker 2012年

4
哦,不..我要重新思考另一个原则!谢谢(现在是讽刺的,一次是讽刺的,我都被讽刺了,觉得自己是一个更好的程序员)
Martin Lyne 2013年

11
好答案。绝对可以考虑...但是您所说的基本上不是归结为不使用抽象类吗?
brianestey

32
仅针对规则+1,“在复杂对象的简单网络上就拥有简单对象的复杂网络”。
David Glass

12

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


这不会导致子类中的转换问题吗?如果X具有方法a并且Y继承了X但也具有方法b。当您将测试类子类化时,是否不必将抽象变量转换为Y才能在b上执行测试?
Johnno Nolan 2011年

8

要专门针对抽象类进行单元测试,应出于测试目的,测试base.method()结果和继承时的预期行为而派生它。

您通过调用方法来测试方法,因此通过实现它来测试抽象类...


8

如果您的抽象类包含具有商业价值的具体功能,那么我通常会通过创建一个存根抽象数据的测试双倍方法或通过使用模拟框架为我直接进行测试。我选择哪种方法在很大程度上取决于我是否需要编写特定于测试的抽象方法的实现。

我需要执行此操作的最常见情况是使用模板方法模式时,例如,当我构建某种可扩展的框架以供第三方使用时。在这种情况下,抽象类定义了我要测试的算法,因此测试抽象库比特定实现更有意义。

但是,我认为重要的是,这些测试应仅关注真实业务逻辑具体实现;您不应该对抽象类的实现细节进行单元测试,因为最终会导致脆弱的测试。


6

一种方法是编写一个与您的抽象类相对应的抽象测试用例,然后编写子类化您的抽象测试用例的具体测试用例。对原始抽象类的每个具体子类执行此操作(即,测试用例层次结构反映了您的类层次结构)。请参阅junit收件人手册中的“测试接口”:http ://safari.informit.com/9781932394238/ch02lev1sec6 。

另请参见xUnit模式中的Testcase超类:http : //xunitpatterns.com/Testcase%20Superclass.html


4

我反对“抽象”测试。我认为测试是一个具体的想法,没有抽象的概念。如果您有共同的元素,请将它们放在辅助方法或类中,以供所有人使用。

至于测试抽象测试类,请确保问自己正在测试的是什么。有几种方法,您应该找出适合您的方案的方法。您是否正在尝试在子类中测试新方法?然后让您的测试仅与该方法交互。您是否正在测试基类中的方法?然后可能只为该类配备一个单独的固定装置,并根据需要进行多次测试,分别测试每个方法。


我不想重新测试已经测试过的代码,这就是为什么我要走抽象测试用例之路。我试图在一处测试抽象类中的所有具体方法。
Paul Whelan

7
我不同意至少在某些情况下(很多情况下)将公共元素提取到辅助类中。如果抽象类包含一些具体的功能,我认为直接对该功能进行单元测试是完全可以接受的。
塞斯·佩特里·约翰逊

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来抽象方法。


3

如果具体方法调用任何抽象方法,则该策略将不起作用,并且您需要分别测试每个子类的行为。否则,再次扩展它并按您所描述的那样对抽象方法进行存根就可以了,只要再次将抽象类的具体方法与子类分离即可。


2

我想您可能想测试抽象类的基本功能...但是,最好扩展该类而不覆盖任何方法,并为该抽象方法进行最少的工作量模拟。


2

使用抽象类的主要动机之一是在应用程序中启用多态性-即:您可以在运行时替换不同的版本。实际上,这与使用接口几乎相同,不同之处在于抽象类提供了一些常用的管道,通常称为“ 模板”模式

从单元测试的角度来看,有两件事要考虑:

  1. 您的抽象类与相关类的交互。对于这种情况,使用模拟测试框架非常理想,因为它表明您的抽象类可以与其他类很好地协作。

  2. 派生类的功能。如果您具有为派生类编写的自定义逻辑,则应单独测试这些类。

编辑:RhinoMocks是一个了不起的模拟测试框架,可以通过动态派生您的类在运行时生成模拟对象。这种方法可以为您节省大量的手工编码派生类的时间。


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.

       }

   }

1

如果抽象类适合您的实现,请测试(如上所述)一个派生的具体类。您的假设是正确的。

为避免将来造成混乱,请注意,此具体的测试类不是模拟的,而是伪造的

严格来说,模拟是由以下特征定义的:

  • 使用模拟代替正在测试的主题类的每个依赖关系。
  • 模拟是接口的伪实现(您可能还记得,通常应将依赖项声明为接口;可测试性是其主要原因之一)
  • 模拟的接口成员的行为-无论是方法还是属性-均在测试时提供(同样,通过使用模拟框架)。这样,您可以避免将要测试的实现与其依赖项的实现耦合在一起(它们都应具有自己的离散测试)。

1

按照@ patrick-desjardins的回答,我实现了abstract及其实现类@Test,如下所示:

抽象类-ABC.java

import java.util.ArrayList;
import java.util.List;

public abstract class ABC {

    abstract String sayHello();

    public List<String> getList() {
        final List<String> defaultList = new ArrayList<>();
        defaultList.add("abstract class");
        return defaultList;
    }
}

由于不能实例化Abstract类,但可以将其抽象化,因此具体类DEF.java如下:

public class DEF extends ABC {

    @Override
    public String sayHello() {
        return "Hello!";
    }
}

@Test类可以测试抽象方法和非抽象方法:

import org.junit.Before;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.contains;
import java.util.Collection;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;

import org.junit.Test;

public class DEFTest {

    private DEF def;

    @Before
    public void setup() {
        def = new DEF();
    }

    @Test
    public void add(){
        String result = def.sayHello();
        assertThat(result, is(equalTo("Hello!")));
    }

    @Test
    public void getList(){
        List<String> result = def.getList();
        assertThat((Collection<String>) result, is(not(empty())));
        assertThat(result, contains("abstract class"));
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.