什么是单元测试流程控制模式

时间:2009-12-10 00:35:17

标签: unit-testing

我有一种检查一些假设的方法,要么遵循快乐的路径,要么沿着不愉快的路径终止。我要么设计得很差,要么我错过了测试流量控制的方法。

if (this.officeInfo.OfficeClosed)
{
    this.phoneCall.InformCallerThatOfficeIsClosedAndHangUp();
    return;
}
if (!this.operators.GetAllOperators().Any())
{
    this.phoneCall.InformCallerThatNoOneIsAvailableAndSendToVoicemail();
    return;
}
Call call=null;
forach(var operator in this.operators.GetAllOperators())
{
    call = operator.Call();
    if(call!=null) {break;}
}

等等。我注入了我的依赖项。我有我的模拟器。我可以确保这个或那个被调用,但我不知道如何测试“返回”发生。如果TDD意味着在没有测试失败的情况下我不会写一行,我就会陷入困境。

你会如何测试?或者有没有办法写它让它更可测试?

更新:已经发布了几个答案,说我应该测试结果调用,而不是流量控制。我对这种方法的问题是,每个测试都需要设置和测试其他测试的状态和结果。这似乎非常笨拙和脆弱。我不应该单独测试第一个if子句,然后单独测试第二个吗?我是否真的需要一组对数扩展的测试,开始看起来像Method_WithParameter_DoesntInvokeMethod8IfMethod7IsTrueandMethod6IsTrueAndMethod5IsTrueAndMethod4IsTrueAndMethod3IsFalseAndMethod2IsTrueAndMethod1isAaaaccck()?

6 个答案:

答案 0 :(得分:4)

我认为您要测试程序的输出:例如,当this.officeInfo.OfficeClosed时,程序会调用this.phoneCall.InformCallerThatOfficeIsClosedAndHangUp()并且不会调用其他方法,例如this.operators.GetAllOperators()

我认为你的测试通过询问它们的模拟对象(phoneCall等)来调用它们的方法,或者通过让它们在意外调用它们的任何方法时抛出异常来做到这一点。 / p>

一种方法是创建程序输入的日志文件(例如'OfficeClosed返回true')并输出:然后运行测试,让测试生成日志文件,然后断言生成的日志文件的内容与该测试的预期日志文件内容相匹配。

答案 1 :(得分:3)

我不确定这是否是正确的做法。您关心方法是否产生预期结果,而不一定是控制如何“流过”特定方法。例如,如果调用phoneCall.InformCallerThatOfficeIsClosedAndHangUp,那么我假设某些结果被记录在某处。因此,在您的单元测试中,您将声明结果确实已记录(通过检查数据库记录,文件等)。

话虽如此,确保您的单元测试确实涵盖您的代码非常重要。为此,您可以使用NCover之类的工具来确保所有代码都被执行。它将生成一个覆盖率报告,该报告将准确显示您的单元测试执行哪些行,更重要的是,哪些不是。

答案 2 :(得分:1)

不要测试流量控制,只测试预期的行为。也就是说,单元测试并不关心实现细节,只是方法的行为与方法的规范相匹配。因此,如果Add(int x, int y)应在输入4上生成结果x = 2, y = 2,那么请测试输出为4,但不要担心Add如何产生结果

换句话说,单元测试在实现细节和重构下应该是不变的。但是如果你在单元测试中测试实现细节,那么你不能在不破坏单元测试的情况下进行重构。例如,如果您实现方法GetPrime(int k)以返回k素数,则检查GetPrime(10)是否返回29但不测试方法内的流控制。如果使用SieveofEratóstenes实现GetPrime并测试了方法内部的流量控制,然后重构使用Atkin的Sieve,则单元测试将中断。同样重要的是GetPrime(10)返回29,而不是它如何做。

答案 3 :(得分:1)

你可以去弹道并使用策略模式。有一个接口IHandleCall的东西,有一个单独的void方法DoTheRightThing(),以及3个类HandleOfficeIsClosed,HandleEveryoneIsBusy,HandleGiveFirstOperatorAvailable,它们实现了接口。然后有类似的代码:

IHandleCall handleCall;
if (this.officeInfo.OfficeClosed)
{
    handleCall = new HandleOfficeIsClosed();
}
else if other condition
{
handleCall = new OtherImplementation();
}
handleCall.DoTheRightThing();
return;

这样你就可以摆脱方法中的多个返回点。请注意,这是一个非常脏的大纲,但基本上在那时你应该将if / else提取到某个工厂,然后你唯一需要测试的是你的类调用工厂,而handleCall.DoTheRightThing()是叫 - (当然工厂返回正确的策略)。

在任何情况下,由于您已经防范了没有可用的操作员,您可以将结尾简化为:

var operator = this.operators.FindFirst();
call = operator.Call();   

答案 4 :(得分:1)

如果你坚持使用TDD这是一件好事:这意味着TDD 驱动你的设计,你正在研究如何改变它,以便你可以测试它。

你可以: 1)验证状态:在SUT执行后检查SUT状态或 2)验证行为:检查模拟对象调用是否符合测试期望

如果您不喜欢这些方法中的任何一种在测试中的反映,那么就该重构代码了。

答案 5 :(得分:0)

Aaron FengK. Scott Allen描述的模式可以解决我的问题以及它的可测性问题。我看到的唯一问题是它需要预先执行所有计算。需要在所有条件之前填充决策数据对象。除非它需要连续往返持久性存储,否则这很好。