我在思考如何对抽象类和继承自抽象类的类进行单元测试。
我应该通过继承抽象类,将其抽象方法替换为存根(stub),然后测试所有具体方法吗?然后只测试我覆盖的方法,并在继承我的抽象类的对象的单元测试中测试抽象方法?
我是否应该有一个抽象的测试用例来测试抽象类的方法,并在继承抽象类的对象的测试用例中扩展此类?
请注意,我的抽象类具有一些具体方法。
我在思考如何对抽象类和继承自抽象类的类进行单元测试。
我应该通过继承抽象类,将其抽象方法替换为存根(stub),然后测试所有具体方法吗?然后只测试我覆盖的方法,并在继承我的抽象类的对象的单元测试中测试抽象方法?
我是否应该有一个抽象的测试用例来测试抽象类的方法,并在继承抽象类的对象的测试用例中扩展此类?
请注意,我的抽象类具有一些具体方法。
抽象基类有两种使用方式:
你正在专门化你的抽象对象,但是所有客户端都将通过其基本接口使用派生类。
您正在使用抽象基类来消除设计中对象中的重复,并且客户端通过它们自己的接口使用具体实现。!
解决方案1 - 策略模式
如果您遇到第一种情况,则实际上有一个接口由抽象类中的虚拟方法定义,您的派生类正在实现该接口。
您应该考虑使其成为真正的接口,将抽象类更改为具体类,并在其构造函数中获取此接口的实例。然后,您的派生类变为这个新接口的实现。
这意味着您现在可以使用新接口的模拟实例测试以前的抽象类,每个新实现都通过现在的公共接口。一切都很简单和可测试。
解决方案2
如果您遇到第二种情况,则您的抽象类作为辅助类起作用。
查看它包含的功能。看看是否可以将其中任何内容推送到正在操作的对象上,以最小化这种重复。如果仍有剩余内容,请考虑将其作为辅助类,使具体实现在其构造函数中接受并删除其基类。
这再次导致简单且易于测试的具体类。
作为规则
复杂的简单对象网络优于复杂对象的简单网络。
可扩展可测试代码的关键是小型构建块和独立连接。
更新:如何处理两者的混合?
有可能存在一个基类同时执行这两个角色...即它有一个公共接口,并且具有受保护的辅助方法。如果是这种情况,那么你可以将辅助方法提取到一个类中(方案2),并将继承树转换为策略模式。
如果你发现基类实现了一些直接的方法并且其他方法是虚拟的,那么你仍然可以将继承树转换为策略模式,但我认为这也表明责任没有正确对齐,可能需要重构。
更新2:作为一个过渡步骤的抽象类(2014/06/12)
我最近遇到了这样一种情况,我使用了抽象类,所以我想探讨一下原因。
我们有一个标准格式用于配置文件。这个特定的工具有3个按照该格式排布的配置文件。我想要为每个设置文件创建一个强类型类,这样通过依赖注入,一个类就可以请求它关心的设置。
我通过创建一个抽象基类来实现这个目标,该基类知道如何解析设置文件格式,派生类公开相同的方法,但是封装了设置文件的位置。
我本可以写一个"SettingsFileParser",由3个类进行包装,然后通过基类委托来公开数据访问方法。但我之所以没有这样做,是因为它会导致3个派生类中有更多的委托代码而不是其他任何东西。
然而...随着这段代码的发展和每个这些设置类的消费者变得更加清晰。每个设置用户将请求一些设置并以某种方式转换它们(由于设置是文本,它们可能将它们包装在对象中或将它们转换为数字等)。随着这一点的发生,我将开始将此逻辑提取到数据操作方法中,并将其推回到强类型设置类中。这将导致每组设置的更高级别接口,最终不再知道它正在处理“设置”。
此时,强类型设置类将不再需要公开底层“设置”实现的“getter”方法。
此时,我不再希望他们的公共接口包括设置访问器方法;因此,我将更改此类,以封装一个设置解析器类,而不是从它继承。
抽象类因此是:我在这一刻避免委托代码的一种方式,并在代码中标记提醒我稍后更改设计。也许我永远不会做到这一点,所以它可能存在很长时间...只有代码能告诉我们。
我发现这对于任何规则都是正确的,比如“没有静态方法”或“没有私有方法”。它们指示代码中的问题...这是好的。它让你寻找你错过的抽象...并在此期间继续为客户提供价值。
我想象这样的规则定义了一个景观,在那里可维护的代码位于山谷中。当添加新行为时,就像雨滴落在您的代码上。最初,您将其放在任何地方...然后进行重构,以允许良好设计的力量推动行为周围,直到所有内容都在山谷中结束。
编写一个Mock对象并仅用于测试。它们通常非常非常非常简单(继承自抽象类),而且不会更多。然后,在您的单元测试中,您可以调用要测试的抽象方法。
您应该测试包含一些逻辑的抽象类,就像您拥有的所有其他类一样。
我的抽象类和接口处理方式如下:我编写一个测试,将对象用作具体实现。但是类型为X(X是抽象类)的变量在测试中未设置。这个测试类不会被添加到测试套件中,但其子类具有设置方法,可将该变量设置为X的具体实现。这样我就不会重复测试代码。未使用测试的子类可以添加更多测试方法。
如果要对抽象类进行单元测试,您应该派生它以进行测试目的,测试基类.method()的结果以及继承时预期的行为。
通过实现来测试抽象类,就像测试方法一样调用它...
我需要这样做的最常见情况是当我使用模板方法模式时,例如当我正在构建某种可扩展框架,该框架将由第三方使用。在这种情况下,抽象类是定义要测试的算法的内容,因此更有意义的是测试抽象基类而不是特定实现。
但是,我认为这些测试应该专注于真正的业务逻辑的具体实现;您不应该单元测试抽象类的实现细节,因为这会导致脆弱的测试。
还可以参见xUnit patterns中的Testcase Superclass:http://xunitpatterns.com/Testcase%20Superclass.html
当我设置测试抽象类的测试框架时,通常会遵循以下模式:
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表达式来存根抽象方法。
首先,如果抽象类包含一些具体方法,我认为你应该考虑以下示例:
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.
}
}