我想知道如何单元测试抽象类,以及扩展抽象类的类。
我是否应该通过扩展抽象类来测试它,去掉抽象方法,然后测试所有的具体方法?然后只测试我重写的方法,并在单元测试中测试扩展抽象类的对象的抽象方法?
我是否应该有一个抽象测试用例,可以用来测试抽象类的方法,并在测试用例中为扩展抽象类的对象扩展该类?
注意,我的抽象类有一些具体的方法。
我想知道如何单元测试抽象类,以及扩展抽象类的类。
我是否应该通过扩展抽象类来测试它,去掉抽象方法,然后测试所有的具体方法?然后只测试我重写的方法,并在单元测试中测试扩展抽象类的对象的抽象方法?
我是否应该有一个抽象测试用例,可以用来测试抽象类的方法,并在测试用例中为扩展抽象类的对象扩展该类?
注意,我的抽象类有一些具体的方法。
当前回答
如果您的抽象类包含具有业务价值的具体功能,那么我通常会通过创建一个测试双存根来直接测试它,或者使用一个模拟框架来为我做这件事。我选择哪一个很大程度上取决于我是否需要编写抽象方法的特定于测试的实现。
我需要这样做的最常见的场景是当我使用模板方法模式时,例如当我正在构建某种将由第三方使用的可扩展框架时。在本例中,抽象类定义了我想要测试的算法,因此测试抽象基类比测试特定实现更有意义。
然而,我认为重要的是这些测试应该只关注真实业务逻辑的具体实现;您不应该单元测试抽象类的实现细节,因为您最终会得到脆弱的测试。
其他回答
这是我在设置一个测试抽象类的工具时通常遵循的模式:
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
如果您有第二种情况,那么您的抽象类将作为辅助类工作。
看看它包含的功能。看看是否可以将其中的任何一部分推到被操纵的对象上,以最小化复制。如果您还有剩余的东西,考虑将其作为一个helper类,您的具体实现使用它的构造函数并删除它们的基类。
这再次导致了简单且易于测试的具体类。
一般来说
喜欢简单对象的复杂网络,而不是复杂对象的简单网络。
可扩展可测试代码的关键是小的构建块和独立的连接。
更新:如何处理两者的混合?
有可能有一个基类同时扮演这两个角色……即:它有一个公共接口,并有受保护的helper方法。如果是这种情况,那么您可以将辅助方法分解为一个类(场景2),并将继承树转换为策略模式。
如果您发现一些方法是基类直接实现的,而另一些方法是虚的,那么您仍然可以将继承树转换为策略模式,但我也认为这是一个很好的指标,说明职责没有正确对齐,可能需要重构。
更新2:抽象类作为垫脚石(2014/06/12)
前几天我遇到了一个使用抽象的情况,所以我想探讨一下为什么。
我们的配置文件有一个标准格式。这个特殊的工具有3个配置文件都是这种格式。我希望每个设置文件都有一个强类型类,这样通过依赖注入,类就可以请求它所关心的设置。
我通过一个抽象基类来实现这一点,它知道如何解析设置文件格式和派生类,这些派生类公开了这些相同的方法,但封装了设置文件的位置。
我本可以编写一个由3个类包装的“SettingsFileParser”,然后委托给基类以公开数据访问方法。我选择不这样做,因为这将导致3个派生类,其中的委托代码比其他任何类都多。
However... as this code evolves and the consumers of each of these settings classes become clearer. Each settings users will ask for some settings and transform them in some way (as settings are text they may wrap them in objects of convert them to numbers etc.). As this happens I will start to extract this logic into data manipulation methods and push them back onto the strongly typed settings classes. This will lead to a higher level interface for each set of settings, that is eventually no longer aware it's dealing with 'settings'.
此时,强类型设置类将不再需要公开底层“设置”实现的“getter”方法。
在这一点上,我将不再希望他们的公共接口包括设置访问器方法;所以我将改变这个类封装一个设置解析器类,而不是从它派生。
因此,对于我来说,抽象类是一种暂时避免委托代码的方法,也是代码中的一个标记,用来提醒我稍后更改设计。我可能永远也见不到它了,所以它可能会活很久……只有密码能告诉我们。
我发现这适用于任何规则……比如“没有静态方法”或者“没有私有方法”。它们表明代码中有一种气味……这很好。它让你不断寻找你错过的抽象概念……同时让你继续为你的客户提供价值。
我想象像这样的规则定义了一个景观,可维护的代码生活在山谷中。当你添加新的行为时,它就像雨水降落在你的代码上。一开始你把它放在它落地的地方。然后你重构,让优秀设计的力量推动行为,直到一切都在山谷中结束。
我猜你可能想测试一个抽象类的基本功能…但是最好的方法是扩展类而不重写任何方法,并尽可能少地模拟抽象方法。
如果抽象类适合于您的实现,则测试(如上所述)派生的具体类。你的假设是正确的。
为了避免将来的混乱,请注意这个具体的测试类不是模拟的,而是假的。
严格来说,mock由以下特征定义:
A mock is used in place of each and every dependency of the subject class being tested. A mock is a pseudo-implementation of an interface (you may recall that as a general rule, dependencies should be declared as interfaces; testability is one primary reason for this) Behaviors of the mock's interface members -- whether methods or properties -- are supplied at test-time (again, by use of a mocking framework). This way, you avoid coupling of the implementation being tested with the implementation of its dependencies (which should all have their own discrete tests).
如果具体方法调用任何抽象方法,该策略将不起作用,您将希望分别测试每个子类的行为。否则,只要抽象类的具体方法与子类解耦,就可以像您所描述的那样扩展它并存根抽象方法。