【问题标题】:How to unit test that object was passed with correct state?如何单元测试该对象以正确的状态通过?
【发布时间】:2017-08-13 16:55:58
【问题描述】:

假设有这样的功能:

function a() 
{
    $entity = $this->getEntity();

    $entity->setSomePrivateVar();

    $service = $this->getService();

    $service->doSomething($entity);

}

我想测试一下

 $service->doSomething($entity);

使用正确的 $entity 调用。

$entity 调用 setSomePrivateVar()

在实际的应用程序代码中我做了这样的事情:

获取实体的模拟并测试是否调用了 setSomePrivateVar。

获取 $service 的 mock 并测试是否使用参数 $entity 调用了 doSomething()。

看起来不错。

但问题是 - 如果我重构代码并首先在服务上调用 doSomething(),然后在 $entity 上调用 setSomePrivateVar(),测试仍然通过。

但是这个函数现在是错误的,因为 doSomething 依赖于由 setSomePrivateVar() 设置的 $entity 私有字段。

例如,我将重构为:

function a() 
{
    $entity = $this->getEntity();

    $service = $this->getService();

    $service->doSomething($entity);

    // this line moved
    $entity->setSomePrivateVar();

}

所以看起来 PhpUnit 没有检查 $entity 私有字段。如果它是例如数组,那么 with() 函数会看到传递的数组与预期的不同。

那么我如何测试 doSomething() 以正确的状态获取 $entity (在将实体传递给 doSomething() 之前已在实体上调用 setSomePrivateVar() )?

也许它与 $entity 被嘲笑有关。

更新现实世界的例子

public function setNotifyUsers(AnnualConsolidation $consolidation, $status)
{
    $consolidation->setNotifyUsers($status);    // if move this method after the flush(), tesst does not fail

    $this->entityManager->persist($consolidation);
    $this->entityManager->flush();
}


public function testNotifyUsers()
{
    $consolidation = $this->getMockBuilder(AnnualConsolidation::class)
        ->setMethods(['setNotifyUsers'])
        ->getMock();

    $consolidation
        ->expects($this->once())
        ->method('setNotifyUsers')
    ;

    $this->entityManager
        ->expects($this->at(0))
        ->method('persist')
        ->with($consolidation)
    ;

    $this->entityManager
        ->expects($this->at(1))
        ->method('flush')
    ;

    /** @var AnnualConsolidation $consolidation */
    $this->consolidationsService->setNotifyUsers($consolidation, true);
}

我们正在讨论以这种方式测试 setNotifyUsers 方法是否更好。我试图在不访问数据库的情况下进行测试。有人认为这可能需要通过命中数据库进行测试,因为如果在不改变逻辑的情况下重构方法,则可能需要测试来重构。另一方面 - 这种方法不太可能被重构那么多。

但也许还有一种方法可以测试在persist()之后调用flush()而不告诉索引,因为在其他示例中,在persist之前添加一些调用之后可能需要更新索引,因此可能是工作量太大,无法保持测试正常运行。

但是对于这个主题 - 首先我想知道如何使测试失败 - 如果我在 flush() 之后移动 setNotifyUsers。 测试没有失败。而如果我们使用命中数据库进行测试 - 我们会看到 $consolidation 状态没有更新。

有人告诉检查,断言传递给persist方法的内容。我还没有尝试过,但我不确定在模拟 $consolidation 上这是否可行。模拟的 $consolidation 是否像真正的 $consolidation 那样具有某种状态?

【问题讨论】:

  • 您能否添加您的测试代码以更好地了解您如何尝试测试。我的猜测是 doSomething 将需要 $entity 中的私有变量?所以在函数“doSomething”中你会调用 $entity->getSomePrivateVar() ?

标签: unit-testing phpunit


【解决方案1】:

正如你在问题中所说的

有人告诉他检查,断言传递给persist方法的内容。

这将是要走的路,但是您的代码使这变得相当困难,我认为您应该进行一些重构以使代码可测试。

首先,您的方法称为“setNotifyUsers”,但它实际上执行了 2 个操作,即调用合并对象上的 setNotifyUsers 并保存/保留此数据。 在我看来,这是 2 种不同的动作,应该属于 2 种不同的方法。如果你像这样写它可以帮助你的测试:

public function setNotifyUsers(AnnualConsolidation $consolidation, $status) {
  $consolidation->setNotifyUsers($status);    
}

public function persistConsolidation(AnnualConsolidation $consolidation) {
  $this->entityManager->persist($consolidation);
  $this->entityManager->flush();
}

您可以分别测试 setNotifyUser 和 persistConsolidation 并为调用这些函数的部分编写功能测试(使用solidationsService 的方法) 那么您可以使用 at() 功能来查看这些函数是否以正确的顺序调用。

但是: 其次,您将状态作为合并到此功能的唯一理由将它们加在一起。我不认为类似的东西属于服务,而是属于方法 调用该服务。 移动该功能将再次给您带来麻烦,因为您无法测试调用它们的顺序。

但是你不需要使用 mockBuilder 来做一个测试替身。 除了使用 $this->getMockBuilder 之外,您还可以创建一个 FakeConsolidation 来实际为您保存数据

那么您还需要为 AnnualConsolidation 模拟,因为您希望能够检查该值是否设置正确。

class FakeConsolidation extends AnnualConsolidation {

  protected $id; 
  proteced $status;

  public function getId() {
    return $this->id;
  }

  public function setId($id) {
     $this->id = $id;
  }

  public function setNotifyUsers($status) {
    $this->status = $status;
  }

  public function shouldNotifyUsers() {
    $this->status
  } 
}

现在,因为您将向具有状态的持久化对象提供一个对象,所以我们可以在“with”部分检查该状态。

当然,我并不确切知道您的代码的结构,所以我做了一些假设,只是在需要的地方进行调整并使用您拥有的接口。

这样你甚至可以测试你在这个问题中提出的代码:

class SomethingTest extends PHPUnit_Framework_TestCase {
  private $consolidationsService;
  private $entityManager;

  /**
   * {@inheritdoc}
   */
  public function setUp() {
    $this->entityManager = $this->getMockBuilder(EntityManager::class)->getMock();
    $this->consolidationsService = new ConsolidationsService($this->entityManager);
  }

  public function testNotifyUsers() {
    $consolidation = new FakeConsolidation();
    $consolidation->setId(1);
    $this->entityManager
      ->expects($this->at(0))
      ->method('persist')
      ->with($this->callback(
          function($savedConsolidation) {
            return $savedConsolidation->shouldNotifyUsers() === true;
          }
      ));

    $this->entityManager
      ->expects($this->at(1))
      ->method('flush');

    /** @var AnnualConsolidation $consolidation */
    $this->consolidationsService->setNotifyUsers($consolidation, TRUE);
  }

}

现在,当您将 setNotifyUsers 移到persist 下方时

with($this->callback(
              function($savedConsolidation) {
                return $savedConsolidation->shouldNotifyUsers() === true;
              }
          )); 

您的测试将失败,因为尚未设置状态。

【讨论】:

  • 我不喜欢有两种不同的方法。我可以有 2 种不同的私有方法,但我仍然希望有一种方法来调用这 2 种私有方法,但这与我写的只有一种方法相同。拥有一种方法很好,因为我可以重复使用它。现在,如果有 2 种方法,在我需要 setNotifyUsers() 的每个地方,我都需要复制代码。重复是不好的,不是吗?顺便说一句,功能测试是指像行为或代码接收 ir DBUnit?如果使用这些,那么没有问题,只是这些测试需要更多时间来执行。
  • 两种不同的方法,其中一种建议但不是唯一的建议,即使您将代码保持原样,假货的解决方案也将起作用您也可以使用 phpunit 进行功能测试,这只是意味着您不再测试单个代码单元,但更多功能请参阅stackoverflow.com/questions/2741832/… 如果您使用数据库,它将解决您的问题,但是假货的解决方案应该可以工作,但是我需要检查一些我可能会犯错误的东西,如果 soi 将更新遮阳篷
  • @Darius.V 我改变了我的答案,我忘了对象总是作为参考,所以你的测试仍然会成功。我在查看更新中将答案更改为不使用回调
猜你喜欢
  • 2016-06-09
  • 1970-01-01
  • 2021-07-04
  • 2023-03-20
  • 1970-01-01
  • 2017-12-01
  • 1970-01-01
  • 2021-11-06
  • 1970-01-01
相关资源
最近更新 更多