How to unit test abstract classes: extend with stubs?
我想知道如何对抽象类和扩展抽象类的类进行单元测试。
我应该通过扩展抽象类、删除抽象方法,然后测试所有具体方法来测试它吗?然后只测试我重写的方法,并在单元测试中测试扩展我的抽象类的对象的抽象方法?
我应该有一个抽象测试用例来测试抽象类的方法,并在我的测试用例中为扩展抽象类的对象扩展这个类吗?
注意,我的抽象类有一些具体的方法。
使用抽象基类有两种方法。好的。
您正在专门化抽象对象,但所有客户端都将通过其基本接口使用派生类。好的。
您正在使用抽象基类来消除设计中对象中的重复,而客户机通过自己的接口使用具体的实现。好的。
1策略模式解决方案好的。
好的。
如果您有第一种情况,那么实际上您有一个由派生类正在实现的抽象类中的虚拟方法定义的接口。好的。
您应该考虑将此接口设置为真实的接口,将抽象类更改为具体的,并在其构造函数中使用此接口的实例。然后派生类成为这个新接口的实现。好的。
好的。
这意味着您现在可以使用新接口的模拟实例测试以前的抽象类,并通过现在的公共接口测试每个新实现。一切都很简单,可以测试。好的。
2解决方案好的。
如果您有第二种情况,那么您的抽象类将作为辅助类工作。好的。
好的。
看看它包含的功能。看看是否可以将其中任何一个推到正在被操作的对象上,以最小化这种重复。如果您还有剩余的内容,请考虑将其作为一个助手类,具体实现接受其构造函数并移除其基类。好的。
好的。
这再次导致了具体的类,这些类既简单又容易测试。好的。
作为规则好的。
喜欢简单对象的复杂网络而不是复杂对象的简单网络。好的。
可扩展可测试代码的关键是小的构建块和独立的布线。好的。
更新:如何处理两者的混合物?好的。
可以让一个基类同时执行这两个角色…IE:它有一个公共接口,并且有受保护的helper方法。如果是这样,那么您可以将助手方法分解为一个类(scenario2),并将继承树转换为策略模式。好的。
如果您发现您的基类直接实现了一些方法,而其他方法是虚拟的,那么您仍然可以将继承树转换为策略模式,但我也将它作为一个很好的指示器,表明职责没有正确对齐,可能需要重构。好的。
更新2:抽象类作为垫脚石(2014/06/12)好的。
前几天我有个情况,我用了抽象,所以我想探讨一下为什么。好的。
我们有配置文件的标准格式。这个特定的工具有3个配置文件,都是这种格式。我想要为每个设置文件使用一个强类型的类,这样,通过依赖项注入,一个类可以请求它关心的设置。好的。
我通过拥有一个抽象的基类来实现这一点,该类知道如何解析设置文件格式和派生类,这些类公开了相同的方法,但封装了设置文件的位置。好的。
我可以编写一个"settingsFileParser",这3个类将其打包,然后委托给基类以公开数据访问方法。我还没有选择这样做,因为这样会导致3个派生类中的委托代码比其他类都多。好的。
然而。。。随着代码的发展,这些设置类中的每一个的使用者都变得更加清晰。每个设置用户都会要求一些设置,并以某种方式对其进行转换(因为设置是文本,所以用户可以将它们包装在对象中,以便将它们转换为数字等)。在这种情况下,我将开始将此逻辑提取到数据操作方法中,并将它们推回到强类型设置类中。这将导致每一组设置都有一个更高级别的接口,最终不再知道它在处理"设置"。好的。
此时,强类型设置类将不再需要公开底层"settings"实现的"getter"方法。好的。
此时,我不再希望它们的公共接口包含设置访问器方法;因此,我将更改该类以封装设置分析器类,而不是从中派生。好的。
因此,抽象类是:一种目前避免委托代码的方法,以及代码中的一个标记,提醒我以后更改设计。我可能永远也无法做到,所以它可能会活得很长一段时间…只有密码才能知道。好的。
我发现任何规则都是如此…比如"没有静态方法"或者"没有私有方法"。它们表示代码中有气味…这很好。它让你一直在寻找你错过的抽象…同时让您继续为客户提供价值。好的。
我想象这样的规则定义了一个环境,可维护的代码就住在山谷中。当你添加新的行为时,就像雨点降落在你的代码上。最初你把它放在它落地的地方。然后重构以允许良好设计的力量推动行为,直到所有的行为都结束在山谷中。好的。好啊。
编写一个模拟对象,并将其用于测试。它们通常非常小(从抽象类继承),而不是更多。然后,在单元测试中,可以调用要测试的抽象方法。
您应该测试包含一些逻辑的抽象类,就像所有其他类一样。
我对抽象类和接口所做的工作如下:我编写一个测试,它使用对象作为具体对象。但是类型x(x是抽象类)的变量没有在测试中设置。这个测试类不是添加到测试套件中,而是它的子类,它有一个设置方法,将变量设置为x的具体实现。这样我就不会复制测试代码。如果需要,未使用测试的子类可以添加更多的测试方法。
如果您的抽象类包含具有业务价值的具体功能,那么我通常会直接通过创建一个删除抽象数据的测试双精度表来测试它,或者使用一个模拟框架来为我这样做。我选择哪一个很大程度上取决于我是否需要编写抽象方法的测试特定实现。
最常见的场景是,当我使用模板方法模式时,我需要这样做,例如,当我构建某种可扩展框架时,将由第三方使用。在这种情况下,抽象类定义了我要测试的算法,因此测试抽象基比测试特定的实现更有意义。
但是,我认为这些测试应该只关注真实业务逻辑的具体实现是很重要的;您不应该对抽象类的实现细节进行单元测试,因为您最终会遇到脆弱的测试。
要专门在抽象类上进行单元测试,您应该在继承时为测试目的、测试base.method()结果和预期的行为派生它。
你通过调用一个方法来测试一个方法,所以通过实现一个抽象类来测试它…
一种方法是编写与抽象类相对应的抽象测试用例,然后编写具体的测试用例,将抽象测试用例分为子类。对原始抽象类的每个具体子类都这样做(即,测试用例层次结构反映了类层次结构)。请参见JUnit Recipies手册中的测试接口:http://safari.informit.com/9781932394238/ch02lev1sec6。
另请参见xUnit模式中的testcase超类:http://xUnitPatterns.com/testcase%20superclass.html
这是我在设置用于测试抽象类的工具时通常遵循的模式:
1 2 3 4 5 6 | public abstract class MyBase{ /*...*/ public abstract void VoidMethod(object param1); public abstract object MethodWithReturn(object param1); /*,,,*/ } |
我在测试中使用的版本:
1 2 3 4 5 6 7 8 9 10 11 12 | 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来截取抽象方法。
我会反对"抽象"测试。我认为测试是一个具体的想法,没有抽象的概念。如果您有公共元素,请将它们放在帮助器方法或类中,供每个人使用。
至于测试抽象测试类,一定要问自己测试的是什么。有几种方法,您应该了解在您的场景中什么有效。您是否尝试在子类中测试一个新方法?然后让您的测试只与该方法交互。你在测试你的基类中的方法吗?然后可能只有该类有一个单独的夹具,并根据需要使用尽可能多的测试分别测试每个方法。
如果具体方法调用了任何抽象方法,那么该策略将不起作用,并且您希望分别测试每个子类的行为。否则,如果抽象类的具体方法与子类分离,那么扩展它并删除您所描述的抽象方法就可以了。
首先,如果抽象类包含一些具体的方法,我认为您应该考虑这个例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | 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. } } |
使用抽象类的主要动机之一是在应用程序中启用多态性——即:您可以在运行时替换不同的版本。实际上,这与使用接口非常相似,只是抽象类提供了一些公共管道,通常称为模板模式。
从单元测试的角度来看,有两件事需要考虑:
抽象类与IT相关类的交互。使用模拟测试框架是这个场景的理想选择,因为它表明抽象类与其他类很好地配合。
派生类的功能。如果您有为派生类编写的自定义逻辑,那么应该单独测试这些类。
编辑:Rhinomocks是一个很棒的模拟测试框架,可以通过从类中动态派生来在运行时生成模拟对象。这种方法可以为您节省无数小时的手工编码派生类。
我想您可以测试抽象类的基本功能…但是,通过扩展类而不重写任何方法,并尽量减少对抽象方法的模拟,您可能是最好的选择。
按照@patrick desjardins的回答,我实现了abstract,它是与
抽象类-abc.java
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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如下:
1 2 3 4 5 6 7 |
@测试类测试抽象方法和非抽象方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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接口成员的行为——无论是方法还是属性--在测试时提供(同样,通过使用模拟框架)。这样,您就可以避免将要测试的实现与依赖项的实现耦合(这些依赖项都应该有自己的独立测试)。