493

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

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

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

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

4

14 回答 14

489

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

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

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


解决方案 1 - 策略模式

选项1

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

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

爱电机

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


解决方案 2

如果您有第二种情况,那么您的抽象类正在充当帮助类。

抽象助手

看看它包含的功能。看看是否可以将其中的任何一个推到正在被操纵的对象上以最小化这种重复。如果您还有任何剩余,请考虑使其成为您的具体实现在其构造函数中采用的辅助类并删除它们的基类。

电机助手

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


作为一项规则

喜欢简单对象的复杂网络,而不是复杂对象的简单网络。

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


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

可以有一个基类来执行这两个角色......即:它有一个公共接口,并且有受保护的辅助方法。如果是这种情况,那么您可以将辅助方法分解为一个类(场景2)并将继承树转换为策略模式。

如果你发现你有一些你的基类直接实现的方法,而另一些是虚拟的,那么你仍然可以将继承树转换为策略模式,但我也会把它作为一个很好的指标,表明职责没有正确对齐,并且可能需要重构。


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

前几天我遇到了一个使用abstract的情况,所以我想探讨一下原因。

我们的配置文件有一个标准格式。这个特殊的工具有 3 个配置文件都采用这种格式。我希望每个设置文件都有一个强类型类,因此,通过依赖注入,一个类可以请求它关心的设置。

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

我本可以编写一个“SettingsFileParser”,将 3 个类包装起来,然后委托给基类以公开数据访问方法。我选择不这样做因为它会导致 3 个派生类中的委托代码比其他任何东西都多。

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

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

那时我不再希望他们的公共接口包含设置访问器方法;所以我将改变这个类来封装一个设置解析器类,而不是从它派生。

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

我发现这适用于任何规则......比如“没有静态方法”或“没有私有方法”。它们表明代码中有异味……这很好。它让您一直在寻找您错过的抽象......同时让您继续为您的客户提供价值。

我想象像这样的规则定义了一个景观,可维护的代码存在于山谷中。当您添加新行为时,就像雨点落在您的代码上一样。最初你把它放在它所在的任何地方......然后你重构以允许良好设计的力量推动行为,直到它全部结束在山谷中。

于 2010-06-01T07:01:06.243 回答
282

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

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

于 2008-10-28T13:50:25.077 回答
12

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

于 2008-10-28T13:47:34.723 回答
10

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

你通过调用一个方法来测试它,所以通过实现它来测试一个抽象类......

于 2008-10-28T13:39:22.667 回答
9

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

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

然而,我认为这些测试应该只关注真实业务逻辑的具体实现是很重要的;你不应该对抽象类的实现细节进行单元测试,因为你最终会得到脆弱的测试。

于 2008-10-28T14:19:18.250 回答
6

一种方法是编写一个与您的抽象类相对应的抽象测试用例,然后编写子类化您的抽象测试用例的具体测试用例。对原始抽象类的每个具体子类执行此操作(即,您的测试用例层次结构反映了您的类层次结构)。请参阅 junit recipies 书中的测试接口:http: //safari.informit.com/9781932394238/ch02lev1sec6https://www.manning.com/books/junit-recipeshttps://www.amazon.com/JUnit-Recipes-Practical-Methods-Programmer/dp/1932394230如果您没有 safari 帐户。

另请参阅 xUnit 模式中的测试用例超类:http: //xunitpatterns.com/Testcase%20Superclass.html

于 2008-12-04T07:41:05.363 回答
4

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

至于测试一个抽象测试类,请确保你问自己你正在测试什么。有几种方法,您应该找出在您的场景中有效的方法。您是否正在尝试在您的子类中测试一种新方法?然后让您的测试仅与该方法交互。您是否正在测试基类中的方法?然后可能只为该类有一个单独的夹具,并根据需要使用尽可能多的测试单独测试每个方法。

于 2008-10-28T13:35:55.013 回答
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);
  }
  /*,,,*/
}

如果在我不期望的时候调用了抽象方法,测试就会失败。在安排测试时,我可以很容易地使用 lambdas 来存根抽象方法,这些方法执行断言、抛出异常、返回不同的值等。

于 2008-10-28T14:03:46.703 回答
3

如果具体方法调用该策略不起作用的任何抽象方法,并且您希望分别测试每个子类的行为。否则,如您所描述的那样扩展它并存根抽象方法应该没问题,再次提供抽象类的具体方法与子类分离。

于 2008-10-28T13:36:38.787 回答
2

我想你可能想测试一个抽象类的基本功能......但你可能最好通过扩展类而不覆盖任何方法,并对抽象方法进行最小化的模拟。

于 2008-10-28T13:36:50.863 回答
2

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

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

  1. 您的抽象类与其相关类的交互。使用模拟测试框架非常适合这种情况,因为它表明您的抽象类与其他类配合得很好。

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

编辑:RhinoMocks 是一个很棒的模拟测试框架,它可以在运行时通过从你的类中动态派生来生成模拟对象。这种方法可以为您节省无数小时的手动编写派生类的时间。

于 2008-10-28T14:09:27.503 回答
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.

       }

   }
于 2015-05-16T06:32:51.693 回答
1

在@patrick-desjardins 回答之后,我实现了抽象及其实现类,@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;
    }
}

由于抽象类不能被实例化,但可以被子类化,具体类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"));
    }
}
于 2017-01-23T16:06:34.857 回答
1

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

为避免将来混淆,请注意这个具体的测试类不是模拟的,而是的。

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

  • 使用模拟来代替正在测试的主题类的每个依赖项。
  • 模拟是接口的伪实现(您可能还记得,作为一般规则,依赖项应该声明为接口;可测试性是这样做的主要原因之一)
  • 模拟接口成员的行为——无论是方法还是属性——在测试时提供(同样,通过使用模拟框架)。这样,您可以避免将正在测试的实现与其依赖项的实现耦合(它们都应该有自己的离散测试)。
于 2018-01-24T20:28:38.750 回答