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