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

时间:2008-10-28 13:25:21

标签: java unit-testing testing abstract-class

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

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

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

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

14 个答案:

答案 0 :(得分:415)

答案 1 :(得分:244)

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

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

答案 2 :(得分:11)

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

答案 3 :(得分:8)

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

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

答案 4 :(得分:8)

如果您的抽象类包含具有业务价值的具体功能,那么我通常会通过创建一个存根抽象数据的测试双重来直接测试它,或者通过使用模拟框架为我做这个。我选择哪一个取决于我是否需要编写抽象方法的特定于测试的实现。

我需要这样做的最常见情况是当我使用Template Method pattern时,例如当我构建某种可由第三方使用的可扩展框架时。在这种情况下,抽象类定义了我想要测试的算法,因此测试抽象基础比使用特定实现更有意义。

但是,我认为这些测试应该只关注实际业务逻辑的具体实现;你不应该单元测试抽象类的实现细节因为你最终会进行脆弱的测试。

答案 5 :(得分:6)

一种方法是编写一个与您的抽象类对应的抽象测试用例,然后编写将抽象测试用例子类化的具体测试用例。为原始抽象类的每个具体子类执行此操作(即,您的测试用例层次结构镜像您的类层次结构)。请参阅junit收件簿中的测试界面:http://safari.informit.com/9781932394238/ch02lev1sec6

还可以在xUnit模式中看到Testcase超类:http://xunitpatterns.com/Testcase%20Superclass.html

答案 6 :(得分:4)

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

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

答案 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简单地删除抽象方法。

答案 8 :(得分:3)

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

答案 9 :(得分:2)

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

答案 10 :(得分:2)

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

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

  1. 您的抽象类与其相关类的交互。使用模拟测试框架是这种情况的理想选择,因为它表明您的抽象类可以很好地与其他人一起使用。

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

  3. 编辑:RhinoMocks是一个非常棒的模拟测试框架,可以通过从您的类动态派生来在运行时生成模拟对象。这种方法可以为您节省无数小时的手工编码派生类。

答案 11 :(得分:2)

首先,如果抽象类包含一些具体的方法,我认为你应该这样做考虑这个例子

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;


import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import java.awt.Font;

public class menuFonts extends JFrame {

    public menuFonts() {

        setTitle("Menu Fonts");
        setSize(300, 300);


        JMenuBar fonts = new JMenuBar();
        setJMenuBar(fonts);

       JMenu menu_Face = new JMenu("Face");
        JMenu  menu_Size = new JMenu("Size");
        JMenu  menu_Color = new JMenu("Color");
       fonts.add( menu_Face);
       fonts.add(menu_Size);
        fonts.add(menu_Color);


        JMenuItem menu_Face_arial = new JMenuItem("Arial", 'A');
        JMenuItem menu_Face_tahoma = new JMenuItem("Tahoma", 'T');
        JMenuItem menu_Face_verdana = new JMenuItem("Verdana", 'V');

        JMenuItem menu_Size_12 = new JMenuItem("12");
        JMenuItem menu_Size_14= new JMenuItem("14");
        JMenuItem menu_Size_16 = new JMenuItem("16");

        JMenuItem menu_Color_red = new JMenuItem("Red", 'R');
        JMenuItem menu_Color_blue = new JMenuItem("Blue", 'B');
        JMenuItem menu_Color_green = new JMenuItem("Green", 'G');


        menu_Face.add(menu_Face_arial);
        menu_Face.add(menu_Face_tahoma);
        menu_Face.add(menu_Face_verdana);

            fonts.add(menu_Size);
            menu_Size.add(menu_Size_12);
        menu_Size.add(menu_Size_14);
        menu_Size.add(menu_Size_16);

            fonts.add(menu_Color);
            menu_Color.add(menu_Color_red);
        menu_Color.add(menu_Color_blue);
        menu_Color.add(menu_Color_green);



    }
    public static void main(String[] args) {
        menuFonts me = new menuFonts();
        me.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        me.setVisible(true);
    }
} 

答案 12 :(得分: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"));
    }
}

答案 13 :(得分:0)

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

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

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

  • 使用模拟来代替正在测试的主题类的每个依赖项。
  • 模拟是接口的伪实现(您可能会记得,作为一般规则,依赖项应该声明为接口;可测试性是此的一个主要原因)
  • mock的接口成员的行为 - 无论是方法还是属性 - 在测试时提供(再次,使用模拟框架)。这样,您就可以避免将正在测试的实现与其依赖项的实现耦合(它们应该都有自己的离散测试)。