单元测试无效方法?


170

对不返回任何内容的方法进行单元测试的最佳方法是什么?特别是在C#中。

我真正想测试的是一种采用日志文件并将其解析为特定字符串的方法。然后将字符串插入数据库中。之前没有做过什么,但是对于TDD来说是非常新的,我想知道是否有可能对此进行测试,或者它是否真的没有经过测试。


55
如果这不是您要执行的操作,请不要使用“ TDD”一词。您正在执行单元测试,而不是TDD。如果您正在执行TDD,您将永远不会遇到诸如“如何测试方法”之类的问题。该测试将首先存在,然后问题将是,“如何使该测试通过?” 但是,如果您正在执行TDD,则将为测试编写代码(而不是相反),并且最终您将最终回答自己的问题。由于TDD,您的代码格式将有所不同,并且永远不会发生此问题。只是澄清一下。
Suamere 2014年

Answers:


151

如果某个方法不返回任何内容,则为以下之一

  • 必要的 -您要让对象对自己做某事..例如,更改状态(不希望任何确认..它假定将要完成)
  • 信息性 -只是通知某人某件事(没有预期的动作或响应)。

命令式方法-您可以验证任务是否实际执行。验证状态更改是否确实发生。例如

void DeductFromBalance( dAmount ) 

可以通过验证此消息后的余额是否确实小于初始值dAmount来进行测试

信息方法-作为对象公共接口的成员很少,因此通常不进行单元测试。但是,如果必须,您可以验证是否对通知进行了处理。例如

void OnAccountDebit( dAmount )  // emails account holder with info

可以通过验证是否已发送电子邮件进行测试

发布有关您的实际方法的更多详细信息,人们将能够更好地回答。
更新:您的方法正在做两件事。实际上,我将其分为两种可以独立测试的方法。

string[] ExamineLogFileForX( string sFileName );
void InsertStringsIntoDatabase( string[] );

通过为第一种方法提供虚拟文件和期望的字符串,可以轻松地验证String []。第二个有点棘手..您可以使用Mock(在模拟框架上使用Google模仿或搜索stackoverflow)来模仿数据库,也可以命中实际的数据库并验证字符串是否插入正确的位置。检查此主题以获取一些好书...如果您处于紧要关头,我建议进行实用单元测试。
在代码中,它将像

InsertStringsIntoDatabase( ExamineLogFileForX( "c:\OMG.log" ) );

1
嘿gishu,好的答案。您提供的示例...不是更多的集成测试...吗?如果是这样,问题仍然存在,如何真正测试无效方法....也许这是不可能的?
安迪

2
@andy-取决于您对“集成测试”的定义。命令式方法通常会更改状态,因此可以通过询问对象状态的单元测试来验证。信息方法可以通过插入模拟侦听器/协作器的单元测试来验证,以确保测试对象发出正确的通知。我认为两者都可以通过单元测试进行合理测试。
Gishu

@andy可以通过访问器接口模拟/分离数据库,从而允许通过传递给模拟对象的数据来测试操作。
彼得·盖格

62

测试其副作用。这包括:

  • 是否抛出任何异常?(如果应该,请检查它是否可以。如果不应该,请尝试一些极端的情况,如果您不小心,可能会很明显-空参数是最明显的事情。)
  • 它的参数是否很好玩?(如果它们是可变的,是否在不应该突变的时候对它们进行突变,反之亦然?)
  • 它对调用它的对象/类型的状态有正确的影响吗?

当然,还有如何限制多少你可以测试一下。例如,通常无法使用所有可能的输入进行测试。进行实用的测试-足以使您确信代码已正确设计并正确实现,并且足以充当调用者可能期望的补充文档。


31

与往常一样:测试该方法应该做什么!

是否应该在某个地方更改全局状态(呃,代码气味!)?

它应该调用一个接口吗?

使用错误的参数调用时是否应该引发异常?

使用正确的参数调用时,它应该不会抛出异常吗?

应该是 ...?


11

无效的返回类型/子例程是旧消息。大约8年以来,我还没有做过Void返回类型(除非我非常懒惰)(从回答这个问题开始,所以在问这个问题之前不久)。

而不是像这样的方法:

public void SendEmailToCustomer()

创建一个遵循Microsoft的int.TryParse()范例的方法:

public bool TrySendEmailToCustomer()

从长远来看,也许您的方法不需要返回任何信息以供使用,但是对调用者来说,在执行其工作之后返回方法的状态非常有用。

同样,布尔不是唯一的状态类型。在很多情况下,先前制作的子例程实际上可以返回三个或更多不同的状态(良好,正常,不良等)。在这种情况下,您只需要使用

public StateEnum TrySendEmailToCustomer()

但是,尽管Try-Paradigm在某种程度上回答了有关如何测试无效收益的问题,但还有其他考虑因素。例如,在“ TDD”周期中/之后,您将在“重构”,并且注意到您正在用自己的方法做两件事……从而破坏了“单一责任原则”。因此,首先应注意这一点。其次,您可能已经了解了一个依赖性...您正在处理“持久性”数据。

如果要在问题方法中进行数据访问,则需要重构为n层或n层架构。但是我们可以假设,当您说“然后将字符串插入数据库”时,实际上意味着您正在调用业务逻辑层或诸如此类。是的,我们假设。

实例化对象后,您现在了解到对象具有依赖性。这是您需要决定是否要对对象或方法进行依赖注入的时候。这意味着您的构造方法或问题方法需要一个新的参数:

public <Constructor/MethodName> (IBusinessDataEtc otherLayerOrTierObject, string[] stuffToInsert)

现在,您可以接受业务/数据层对象的接口,可以在单元测试期间将其模拟出来,而不必依赖或担心“意外”集成测试。

因此,在您的实时代码中,您传入一个REAL IBusinessDataEtc对象。但是在单元测试中,您传入了MOCK IBusinessDataEtc对象。在该Mock中,您可以包含非接口属性,例如,int XMethodWasCalledCount或诸如此类,其状态在调用接口方法时会更新。

因此,单元测试将遍历您的问题方法,执行其具有的任何逻辑,并在您的IBusinessDataEtc对象中调用一个或两个或一组选定的方法。在单元测试结束时执行断言时,现在有几项要测试。

  1. “子程序”的状态,现在是Try-Paradigm方法。
  2. 模拟IBusinessDataEtc对象的状态。

有关构造级别上的依赖项注入思想的更多信息……与单元测试有关……请查看Builder设计模式。它为您拥有的每个当前接口/类增加了一个接口和类,但是它们非常小,并且增加了巨大的功能以实现更好的单元测试。


这个好答案的第一部分是对所有新手/中级程序员的惊人一般建议。
pimbrouwers 18/09/13

不应该是这样 public void sendEmailToCustomer() throws UndeliveredMailException吗?
A.Emad

1
@ A.Emad好问题,但没有。依靠引发异常来控制代码流是一种众所周知的坏习惯。但是,在void方法中,尤其是在面向对象的语言中,它一直是唯一的选择。微软最古老的替代方案是我所讨论的Try-Paradigm,以及诸如Monads / Maybes之类的功能样式范例。因此,Commands(在CQS中)仍然可以返回有价值的状态信息,而不必依赖于引发(这类似于GOTO(我们知道是不好的))。投掷(和goto)速度慢,难以调试,也不是好习惯。
Suamere

感谢您清理此问题。是C#特定的还是在Java和C ++之类的语言中抛出异常通常是不好的做法?
A.Emad


8

您甚至可以这样尝试:

[TestMethod]
public void ReadFiles()
{
    try
    {
        Read();
        return; // indicates success
    }
    catch (Exception ex)
    {
        Assert.Fail(ex.Message);
    }
}

1
这是我想的最简单的方法。
纳文·潘迪特

5

它会对对象产生一些影响。...查询影响的结果。如果没有可见效果,则不值得进行单元测试!


4

大概该方法有作用,并且不会简单地返回吗?

假设是这种情况,则:

  1. 如果它修改了所有者对象的状态,则应测试该状态是否正确更改。
  2. 如果将某个对象作为参数并修改了该对象,则应测试该对象是否正确修改。
  3. 如果在某些情况下抛出异常,请测试是否正确抛出了这些异常。
  4. 如果其行为根据其自己的对象或某个其他对象的状态而变化,请通过上述三种测试方法之一来预置状态并测试该方法是否具有正确的I。

如果您让我们知道该方法的作用,我可能会更具体。


3

使用Rhino Mocks设置可能发生的呼叫,操作和异常情况。假设您可以模拟或存根方法的某些部分。如果不了解有关方法甚至上下文的某些细节,很难知道。


1
如何进行单元测试的答案永远不应该是找到适合您的第三方工具。但是,当一个人知道如何进行单元测试时,可以使用第三方工具简化测试。
Suamere 2014年

2

取决于它在做什么。如果有参数,请传递模拟,您稍后可以询问是否已使用正确的参数集调用了它们。


同意-验证测试该方法的模拟程序的行为将是一种方法。
杰夫·舒马赫

0

无论您使用哪种实例来调用void方法,都可以使用,Verfiy

例如:

就我而言,它_Log是实例,LogMessage是要测试的方法:

try
{
    this._log.Verify(x => x.LogMessage(Logger.WillisLogLevel.Info, Logger.WillisLogger.Usage, "Created the Student with name as"), "Failure");
}
Catch 
{
    Assert.IsFalse(ex is Moq.MockException);
}

Verify抛出一个异常,由于测试将失败的方法的失败?

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.