我正在重构一个类,以便代码是可测试的(使用NUnit和RhinoMocks作为测试和隔离框架)并且发现我发现自己的方法依赖于另一个(即它取决于由其他方法创建的东西) )。类似于以下内容:
public class Impersonator
{
private ImpersonationContext _context;
public void Impersonate()
{
...
_context = GetContext();
...
}
public void UndoImpersonation()
{
if (_context != null)
_someDepend.Undo();
}
}
这意味着要测试UndoImpersonation
,我需要通过调用Impersonate
来设置它(Impersonate已经有几个单元测试来验证它的行为)。这对我来说闻起来很糟糕,但从某种意义上说,从调用此类的代码的角度来看它是有道理的:
public void ExerciseClassToTest(Impersonator c)
{
try
{
if (NeedImpersonation())
{
c.Impersonate();
}
...
}
finally
{
c.UndoImpersonation();
}
}
如果我没有尝试为UndoImpersonation
编写单元测试并发现自己必须通过调用其他公共方法来设置测试,我就不会这样做。那么,这是一种难闻的气味,如果是这样,我该如何解决呢?
代码气味必须是我在编程世界中遇到的最模糊的术语之一。对于一群以自己的工程原理而自豪的人来说,它在不可测量的垃圾方面排名靠前,并且作为无用的措施,就像程序员效率每天的LOC一样。
无论如何,这是我的咆哮,谢谢你的倾听:-)
要回答您的具体问题,我不认为这是一个问题。如果您测试具有前置条件的内容,则需要确保为给定的测试用例首先设置了前置条件。
其中一个测试应该是在没有先设置前置条件的情况下调用它时会发生什么 - 如果调用者没有这样做,它应该优雅地失败或者设置它自己的前置条件。
好吧,有一点上下文要讲,看起来像_someDepend应该在构造函数中初始化。
在实例方法中初始化字段对我来说是一个很大的问题。一个类一旦构建就应该完全可用(即所有方法都可以工作);所以构造函数应该初始化所有实例变量。参见例如在Ward Cunningham的wiki上的single step construction页面。
在实例方法中初始化字段的原因很糟糕,主要是因为它对如何调用方法施加了隐式排序。在您的情况下,TheMethodIWantToTest将根据是否首先调用DoStuff来执行不同的操作。这通常不是你班级用户所期望的,所以这很糟糕:-(。
也就是说,有时这种耦合可能是不可避免的(例如,如果一个方法获取诸如文件句柄之类的资源,则需要另一种方法来释放它)。但即便如此,也应该在一种方法中处理。
如果没有更多背景,那么适用于您的案例很难说清楚。
如果您不考虑可变对象代码自身的气味,必须将对象置于测试所需的状态只是该测试设置的一部分。
这通常是不可避免的,例如在使用远程连接时 - 你必须在调用Open()
之前调用Close()
,并且你不希望Open()
在构造函数中自动发生。
但是,在这样做时你要非常小心,模式是易于理解的 - 例如我认为大多数用户接受这种行为的任何事务,但当他们遇到DoStuff()
和TheMethodIWantToTest()
(无论他们真正称为什么)时可能会感到惊讶)。
通常最佳做法是拥有一个代表当前状态的属性 - 再次查看远程或数据库连接,以获得一致理解的设计示例。
对于房产来说,这是一个很大的禁忌。属性永远不应该关心它们被调用的顺序。如果你有一个简单的值取决于方法的顺序,那么它应该是无参数的方法而不是property-get。
是的,我认为在这种情况下有代码味道。不是因为方法之间的依赖关系,而是因为对象的模糊身份。而不是拥有一个可以处于不同角色状态的Impersonator
,为什么不拥有一个不可变的Persona
?
如果您需要不同的Persona
,只需创建一个新的而不是更改现有对象的状态。如果你之后需要做一些清理,那就让Persona
一次性使用。您可以将Impersonator
类保留为工厂:
using (var persona = impersonator.createPersona(...))
{
// do something with the persona
}
回答标题:在面向对象的编程中,让方法相互调用(链接)是不可避免的,因此在我看来,测试调用另一个方法的方法没有任何问题。毕竟单元测试可以是一个类,它是你正在测试的“单元”。
链接级别取决于对象的设计 - 您可以分叉或级联。
classToTest1.SomeDependency.DoSomething()
classToTest1.DoSomething()
(内部会调用SomeDependency.DoSomething)但正如其他人所提到的那样,绝对保持你的状态初始化在构造函数中,从我所知道的,可能会解决你的问题。