单元测试和静态方法

Posted

技术标签:

【中文标题】单元测试和静态方法【英文标题】:unit testing and Static methods 【发布时间】:2011-08-23 02:13:45 【问题描述】:

阅读并学习单元测试,试图理解 the following post 的含义,它解释了静态函数调用的困难。

我不清楚这个问题。我一直认为静态函数是在类中舍入实用函数的好方法。比如我经常使用静态函数调用来初始化,即:

Init::loadConfig('settings.php');
Init::setErrorHandler(APP_MODE); 
Init::loggingMode(APP_MODE);

// start loading app related objects ..
$app = new App();

// 看完这篇文章,我现在的目标是……

$init = new Init();
$init->loadConfig('settings.php');
$init->loggingMode(APP_MODE);
 // etc ...

但是,我为这门课编写的几十个测试是相同的。我没有改变任何东西,它们仍然都通过了。我做错了吗?

该帖子的作者声明如下:

静态方法的基本问题是它们是过程代码。我不知道如何对程序代码进行单元测试。单元测试假设我可以单独实例化我的应用程序的一部分。在实例化期间,我将依赖项与替换真正依赖项的模拟/友好连接。对于过程式编程,没有什么可以“连接”的,因为没有对象,代码和数据是分开的。

现在,我从帖子中了解到静态方法会创建依赖关系,但不能直观地理解为什么不能像常规方法一样轻松测试静态方法的返回值?

我将避免使用静态方法,但我希望了解静态方法何时有用(如果有的话)。从这篇文章看来,静态方法与全局变量一样邪恶,应尽可能避免。

非常感谢您提供有关该主题的任何其他信息或链接。

【问题讨论】:

【参考方案1】:

Sebastian Bergmann 同意 Misko Hevery 的观点并经常引用他的话:

Stubbing and Mocking Static Methods

单元测试需要接缝,接缝是我们阻止正常代码路径执行的地方,也是我们实现被测类隔离的方式。接缝通过多态工作,我们覆盖/实现类/接口,然后以不同的方式连接被测类以控制执行流程。使用静态方法没有什么可以覆盖的。是的,静态方法很容易调用,但是如果静态方法调用另一个静态方法,就没有办法覆盖被调用的方法依赖了。

静态方法的主要问题是它们引入了耦合,通常是通过将依赖项硬编码到您的消费代码中,使得在单元测试中用存根或模拟替换它们变得困难。这违反了Open/Closed Principle 和Dependency Inversion Principle,这两个SOLID principles。

statics are considered harmful 你说的完全正确。避开他们。

请查看链接以获取更多信息。

更新:请注意,尽管静态仍然被认为是有害的,the capability to stub and mock static methods has been removed as of PHPUnit 4.0

【讨论】:

亲爱的戈登,因为你的帖子很有用,如果可能的话,我很乐意更新你帖子的链接。尤其是显示存根和模拟静态方法的 url。【参考方案2】:

静态方法本身并不比实例方法更难测试。当一个方法(静态或其他方法)调用 other 静态方法时就会出现问题,因为您无法隔离正在测试的方法。这是一个很难测试的典型示例方法:

public function findUser($id) 
    Assert::validIdentifier($id);
    Log::debug("Looking for user $id");  // writes to a file
    Database::connect();                 // needs user, password, database info and a database
    return Database::query(...);         // needs a user table with data

你想用这个方法测试什么?

传递除正整数以外的任何值都会抛出 InvalidIdentifierExceptionDatabase::query() 收到正确的标识符。 找到匹配的用户返回,null 找不到。

这些要求很简单,但您还必须设置日志记录、连接到数据库、加载数据等。Database 类应该单独负责测试它是否可以连接和查询。 Log 类应该对日志做同样的事情。 findUser() 不应该处理任何这些,但必须处理,因为这取决于它们。

如果上述方法改为调用 DatabaseLog 实例上的实例方法,则测试可以传入具有特定于手头测试的脚本返回值的模拟对象。

function testFindUserReturnsNullWhenNotFound() 
    $log = $this->getMock('Log');  // ignore all logging calls
    $database = $this->getMock('Database', array('connect', 'query');
    $database->expects($this->once())->method('connect');
    $database->expects($this->once())->method('query')
             ->with('<query string>', 5)
             ->will($this->returnValue(null));
    $dao = new UserDao($log, $database);
    self::assertNull($dao->findUser(5));

如果findUser() 忽略调用connect()、传递$id 的错误值(上面的5)或返回null 以外的任何值,上述测试将失败。美妙之处在于不涉及数据库,使测试快速而健壮,这意味着它不会因与测试无关的原因(如网络故障或错误的样本数据)而失败。它使您可以专注于真正重要的事情:findUser() 中包含的功能。

【讨论】:

嘿,谢谢!你的布局方式更有意义。还有一个好处,就是模拟对象的例子。几天前我刚刚开始尝试使用这些,但仍然不清楚如何创建各种模拟“状态”。您的示例为我想模拟的一些对象提供了一个很好的起点。 这是我第一次看到一个静态调用的断言,你不使用 $this->asserNull() 有什么原因吗?我发现具有讽刺意味的是,您的最后一行代码示例是一篇文章中的一个静态调用,它处理了测试静态调用的痛苦。 @stefgosselin - 是不是很讽刺,你不觉得吗? :) PHPUnit 的断言是在PHPUnit_Framework_Assert 中定义的静态方法,TestCase 继承了这些方法。虽然 PHP 允许您使用 -&gt; 调用静态方法,但 self:: 更短并且更正确。 我认为这里的教训是使用依赖注入。依赖注入容器允许单个全局对象,因此它消除了对静态类或单例对象的需要。如果 php 有一种很好的方式来传递静态类的依赖项呢?总是必须从一个类中创建一个对象似乎是错误的,特别是如果方法都是纯的。 @hijarian 静态方法在类上调用,而不是对象和代码可以依赖它们。所以代码可以“依赖于类”。【参考方案3】:

我在测试静态方法时没有发现任何问题(至少没有在非静态方法中不存在的问题)。

使用依赖注入将模拟对象传递给被测类。 模拟静态方法可以使用合适的自动加载器或操纵include_path传递给被测类。 后期静态绑定处理调用同一类中的静态方法的方法。

【讨论】:

当您说:可以使用合适的自动加载器或操作 include_path 将模拟静态方法传递给被测类。您的意思是您只需加载所需的类来保存您的测试中的静态方法? @stef 我不会这样称呼它简单地。必须编写一个新类来存根方法调用并重新连接包含路径以替换硬编码的依赖项是很多工作,PHPUnit 的内部模拟设施和test helpers 解决了这个问题。如果您想要“简单”,请不要首先使用静态和硬编码依赖项(就像 Hevery 和 Bergmann 建议的那样)。看看Clean Code Talks 我现在明白了。虽然使用 SUT 的类是可能的 -> 我认为这是硬连线 include_path 的意思,但这不是正确的方法。谢谢你的链接! 通过更改包含路径提供模拟是一个巨大的 PITA,并且不允许每个测试使用不同的模拟能力。另外,这意味着您无法在同一测试会话中测试真实课程。正如你所说,后期静态绑定对一个类调用另一个类中的静态方法没有帮助,而另一个类更常见的是你想要模拟的地方。

以上是关于单元测试和静态方法的主要内容,如果未能解决你的问题,请参考以下文章

单元测试静态方法 C# 程序

04- 软件测试的方法与软件测试分类

使用方法内的静态调用设计Java单元测试

使用 XCTest 在 Objective-C 静态库中创建测试用例

Junit单元测试

静态方法 NSInvocation