如何单元测试抽象类:扩展存根?

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

我是否应该通过扩展抽象类来测试它,去掉抽象方法,然后测试所有的具体方法?然后只测试我重写的方法,并在单元测试中测试扩展抽象类的对象的抽象方法?

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

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

192116 次浏览

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

至于测试抽象测试类,一定要问自己要测试的是什么。有几种方法,您应该找出适合您的场景的方法。您是否试图在子类中测试一个新方法?然后让您的测试只与该方法交互。测试基类中的方法吗?然后可能只针对该类有一个单独的fixture,并使用尽可能多的测试单独测试每个方法。

如果具体方法调用任何抽象方法,该策略将不起作用,您将希望分别测试每个子类的行为。否则,只要抽象类的具体方法与子类解耦,就可以像您所描述的那样扩展它并存根抽象方法。

我猜你可能想测试一个抽象类的基本功能…但是最好的方法是扩展类而不重写任何方法,并尽可能少地模拟抽象方法。

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

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

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

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

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

这是我在设置一个测试抽象类的工具时通常遵循的模式:

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剔除抽象方法,这些方法执行断言、抛出异常、返回不同的值等。

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

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

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

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

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

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

我需要这样做的最常见的场景是当我使用模板方法模式时,例如当我正在构建某种将由第三方使用的可扩展框架时。在本例中,抽象类定义了我想要测试的算法,因此测试抽象基类比测试特定实现更有意义。

然而,我认为重要的是这些测试应该集中在只有实际业务逻辑的具体实现;你不应该单元测试抽象类的实现细节,因为你最终会得到脆弱的测试。

一种方法是编写与抽象类相对应的抽象测试用例,然后编写抽象测试用例的子类的具体测试用例。对原始抽象类的每个具体子类都这样做(例如,您的测试用例层次反映了您的类层次)。参见junit recipes书中的测试接口:http://safari.informit.com/9781932394238/ch02lev1sec6https://www.manning.com/books/junit-recipeshttps://www.amazon.com/JUnit-Recipes-Practical-Methods-Programmer/dp/1932394230如果你没有safari帐户。

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

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

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

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


1策略模式的解决方案

Option1

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

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

IMotor

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


解决方案2

如果您有第二种情况,那么您的抽象类将作为辅助类工作。

AbstractHelper

看看它包含的功能。看看是否可以将其中的任何一部分推到被操纵的对象上,以最小化复制。如果您还有剩余的东西,考虑将其作为一个helper类,您的具体实现使用它的构造函数并删除它们的基类。

Motor Helper

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


一般来说

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

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


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

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

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


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

前几天我遇到了一个使用抽象的情况,所以我想探讨一下为什么。

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

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

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

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

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

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

因此,对于我来说,抽象类是一种暂时避免委托代码的方法,也是代码中的一个标记,用来提醒我稍后更改设计。我可能永远也见不到它了,所以它可能会活很久……只有密码能告诉我们。

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

我想象像这样的规则定义了一个景观,可维护的代码生活在山谷中。当你添加新的行为时,它就像雨水降落在你的代码上。一开始你把它放在它落地的地方。然后你重构,让优秀设计的力量推动行为,直到一切都在山谷中结束。

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

 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.


}


}

在@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"));
}
}

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

为了避免将来的混淆,请注意这个具体的测试类不是mock,而是假的

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

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