【问题标题】:How can I make my PHPUnit tests more succinct, less long?如何使我的 PHPUnit 测试更简洁、更短?
【发布时间】:2012-01-11 15:51:28
【问题描述】:

我正在为我的 Web 应用程序编写的 PHPUnit 测试的长度和不透明性让我感到厌烦。似乎测试中的代码比他们正在测试的代码多一个数量级。

例如,假设我的网站有一个 CatController 对象,此方法在其上:

public function addCat(Default_Model_Cat $cat)
{
    $workflow = $this->catWorkflowFactory->create(array($this->serviceExecutor));
    $workflow->addCat($cat);
}

为了彻底测试它,我必须创建的单元测试是这样的:

public function testAddCat()
{
    $cat = $this->getMockBuilder('Default_Model_Cat')
        ->disableOriginalConstructor()
        ->getMock();
    $controller = $this->getMockBuilder('CatController')
        ->disableOriginalConstructor()
        ->setMethods(array('none'))
        ->getMock();
    $workflow = $this->getMockBuilder('Default_Model_Workflow_Cat')
        ->setMethods(array('addCat'))
        ->disableOriginalConstructor()
        ->getMock();
    $workflow->expects($this->once())
        ->method('addCat')
        ->with($cat);
    $controller->serviceExecutor = $this->getMockBuilder('ServiceExecutor')
        ->disableOriginalConstructor()
        ->getMock();
    $controller->catWorkflowFactory = $this->getMockBuilder('Factory')
        ->disableOriginalConstructor()
        ->setMethods(array('create'))
        ->getMock();
    $controller->catWorkflowFactory->expects($this->once())
        ->method('create')
        ->with($controller->serviceExecutor)
        ->will($this->returnValue($workflow));
    $controller->addCat($cat);
}

有什么语法可以让单元测试更短更易读吗?例如,与其说“这个对象是一个将调用这个方法的模拟对象,当在它上面调用这个方法时,它将用这个参数调用一次并返回这个值”,如果我能只要说once(object->method(argument)) => $returnvalue之类的话。

【问题讨论】:

  • 只是一个想法:我确信这不适用于其他人,但是当我的单元测试看起来越来越荒谬时,我经常发现问题是我的代码设计/分离不理想:)

标签: php unit-testing mocking phpunit


【解决方案1】:

您越能将类设计为在单元测试中可用而不需要被模拟,那么您需要编写的模拟代码就越少。但是对于上面的例子,我的第一反应是这个方法不需要单元测试,因为它并没有真正执行任何逻辑,写完后不会改变。

话虽如此,假设您将需要此类的其他方法中的工作流实例,请将创建它的代码提取到新方法中。这允许您为每个测试模拟该方法,并且只在一个测试中模拟更长的时间。

例如,如果您还有一个 removeCat() 方法,它看起来像这样:

public function addCat(Default_Model_Cat $cat) {
    $this->createWorkflow()->addCat($cat);
}

public function removeCat(Default_Model_Cat $cat) {
    $this->createWorkflow()->removeCat($cat);
}

public function createWorkflow() {
    return $this->catWorkflowFactory->create(array($this->serviceExecutor));
}

上述方法非常短,实际上不需要单元测试,但现在它们会短一点:

public function testAddCat() {
    $cat = $this->getMockBuilder('Default_Model_Cat')
        ->disableOriginalConstructor()
        ->getMock();
    $controller = $this->getMockBuilder('CatController')
        ->disableOriginalConstructor()
        ->setMethods(array('createWorkflow'))
        ->getMock();
    $workflow = $this->getMockBuilder('Default_Model_Workflow_Cat')
        ->setMethods(array('addCat'))
        ->disableOriginalConstructor()
        ->getMock();
    $controller->expects($this->once())
        ->method('createWorkflow')
        ->will($this->returnValue($workflow));
    $workflow->expects($this->once())
        ->method('addCat')
        ->with($cat);
    $controller->addCat($cat);
}

如果控制器中有很多这样的方法,则可以在测试用例中创建辅助方法来创建模拟。最后,您真的需要禁用模拟上的原始构造函数吗?我很少需要自己做。

【讨论】:

  • 我非常不同意“但是对于上面的例子,我的第一反应是这个方法不需要单元测试,因为它并没有真正执行任何逻辑,并且在编写后不会改变。 "它正在调度对其他方法的调用。这几乎是一个方法应该做的所有事情,并且应该有一个测试来确保使用正确的参数分派正确的方法。除此之外,我也不想在没有任何特定需要的情况下模拟 SUT,以方便测试。恕我直言,这是旧代码的解决方法,而不是新代码的好习惯。只是我的几点。不过答案很好:)
  • 理想情况下,测试将使用真实对象而不是模拟对象,并简单地验证是否添加了类别。但是该方法可能出错的几件事将在前面非常明显。任何重构都需要严重量的测试重构并获得边际收益。争取 100% 的代码覆盖率可能成为一种负担,而不是帮助,而简短的方法使其变得不那么必要。这是 Clean Code 的重要经验之一。
  • 对我来说,“清洁代码”这本书是关于拥有小的描述性命名方法,并通过可读性好的代码争取 100% 的代码覆盖率。对我来说,模拟的 SUT 始终是 WTF 因素,并且用例很少(例如,在不引入日历对象的情况下解决“当前时间”问题)。在手头的原始示例中,我认为不需要模拟 SUT,因此我不会介绍它。我也看不到您的方法如何在重构时减少测试采用时间。 3-4 个依赖模拟总是需要时间来适应,尤其是在难以阅读时
  • 书中的一个观点是,比他们测试的代码更复杂的测试是一种代码味道,应该避免——要么通过重构,这样你就不需要测试,要么重新考虑你的代码。设计。例如,我不为 getter 和 setter 编写测试。正如我所说,我不会为上述方法编写测试,但 OP 想要一个并在他们的示例中使用了模拟。我假设他们需要使用模拟,因为无法在单元测试中构建真正的类。鉴于此,以上是我将如何模拟它。
  • David,您的原始回复违背了我所学过的关于单元测试的两个基本原则:(1) 为所有可以执行任何操作的代码编写单元测试(如果代码不执行任何操作,那么删除它)。 (2) 单元测试应该只测试单元,即可以自己测试的最小代码段;除非您专门(并且仅)测试该构造函数,否则不要调用构造函数。 (你同意这些规则吗,还是我在这里走上歧途?)
【解决方案2】:

如果您有一个CatController 对象,则尽可能不要在测试中模拟它。你想测试那个类,所以使用真正的类。

你可以摆脱所有的"setMethod" 电话。默认情况下,所有方法都会被模拟,这就是你想要的。

还有其他一些模拟库可以减少一些人喜欢的模拟代码行数,例如 PhakeMockery,但我没有太多的个人经验。

让我感到有点奇怪的是,您使用公共属性设置了模拟。我本来希望那些进入 Controllers 构造函数。

鉴于这是你可以做到的方法:

public function testAddCat()
{
    $cat = $this->getMockBuilder('Default_Model_Cat')
        ->disableOriginalConstructor()
        ->getMock();

    $workflow = $this->getMockBuilder('Default_Model_Workflow_Cat')
        ->disableOriginalConstructor()
        ->getMock();
    $workflow->expects($this->once())
        ->method('addCat')
        ->with($cat);

    $controller = new CatController(/*if you need params here pass them!*/);
    // You can use this to avoid mocking the object if you want
    // If your tests are more of a usage doc maybe don't
    $controller->serviceExecutor = "My fake Executor";

    $controller->catWorkflowFactory = $this->getMockBuilder('Factory')
        ->disableOriginalConstructor()
        ->getMock();
    $controller->catWorkflowFactory->expects($this->once())
        ->method('create')
        ->with(array("My fake Executor"))
        ->will($this->returnValue($workflow));

    $controller->addCat($cat);
}

让我们在设置中添加一些常见的东西

为了让每个函数更易于阅读,让我们将默认的 mock 放到 setup 中。

public function setUp() {

    $this->controller = new CatController(/*if you need params, pass them!*/);
    $this->serviceExecutor = $this->getMockBuilder('ServiceExecutor')
        ->disableOriginalConstructor()
        ->getMock();
    $this->controller->serviceExecutor = $this->serviceExecutor;
    $this->cat = $this->getMockBuilder('Default_Model_Cat')
        ->disableOriginalConstructor()
        ->getMock();
    $this->workflow = $this->getMockBuilder('Default_Model_Workflow_Cat')
        ->disableOriginalConstructor()
        ->getMock();
    $this->controller->catWorkflowFactory = $this->getMockBuilder('Factory')
        ->disableOriginalConstructor()
        ->getMock();
}

以及方法:

public function testAddCat()
{
    $this->workflow->expects($this->once())
        ->method('addCat')
        ->with($this->cat);

    $this->controller->catWorkflowFactory->expects($this->once())
        ->method('create')
        ->with(array($this->serviceExecutor))
        ->will($this->returnValue($this->workflow));

    $this->controller->addCat($cat);
}

它仍然不是真的漂亮,但我们将它分成更易于管理的块。

安装程序会创建所有假对象,但它们什么都不做(因此它们不会通过任何测试,并且安装时间应该是疏忽

而测试的重点是描述应该发生的事情。


总的来说,我会说“如果使用该类有那么复杂,那么测试表明需要做很多事情可能是件好事”。如果这是一个问题,可能会改变类。使用它的生产罐头也将很难正确设置所有内容。但是许多框架/方法使控制器在这方面“特别”,所以你最了解:)

【讨论】:

  • 不需要每次都调用 setMethods() 很好。我养成了这个习惯,因为当我模拟控制器时,我经常忘记调用它,然后当我调用我在控制器上测试的方法时,我实际上会调用返回 NULL 的模拟。顺便说一句,我模拟了控制器,因为我不想调用它的构造函数,除非我专门测试构造函数(见我上面的评论)。
  • 我不喜欢使用 setUp() 调用;根据我的经验,如果我将大量代码放入 setUp() 中,这意味着我的 SUT 中有很多重复的代码,或者 setUp() 正在设置大多数测试不需要的东西。顺便说一句,我使用公共属性设置模拟是什么意思 - 应该将什么放入构造函数中?
  • @BrianKendig 如果您的 SUT 需要 3-4 个其他对象才能正常工作(如在构造/设置中),我认为在设置中创建这些对象没有任何问题。这就是它通常的用途。创建 SUT 的对象实例。现在,如果设置 SUT 很复杂,那就是另一个问题。另一方面:从外部访问公共属性对我来说就像是另一种形式的 setter 注入 misko.hevery.com/2009/02/19/…。但如果那里好不好,你最清楚:)
猜你喜欢
  • 2011-06-17
  • 2021-12-13
  • 1970-01-01
  • 2018-03-16
  • 1970-01-01
  • 2016-06-06
  • 2017-01-17
  • 1970-01-01
  • 2021-10-29
相关资源
最近更新 更多