如何以正确的状态对该对象进行单元测试?

时间:2017-03-21 19:59:30

标签: unit-testing phpunit

让我们说有这样的功能:

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

    $entity->setSomePrivateVar();

    $service = $this->getService();

    $service->doSomething($entity);

}

我想测试一下

 $service->doSomething($entity);
使用正确的$ entity调用

$ entity调用setSomePrivateVar()

在实际的应用程序代码中,我做了类似的事情:

模拟实体并测试调用setSomePrivateVar。

模拟$ service并测试使用参数$ entity调用doSomething()。

看起来不错。

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

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

例如,我会重构:

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

    $service = $this->getService();

    $service->doSomething($entity);

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

}

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

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

也许它有一些事情要做,$实体被嘲笑。

使用真实世界示例进行更新

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()而不告诉索引,因为在其他示例中,在持久化之前添加一些调用之后可能需要更新索引,因此可能是太多的工作让测试工作。

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

一个人告诉检查,断言传递给persist方法的内容。我还没有尝试,但我不确定是否可以通过嘲笑$合并。嘲讽美元整合是否具有某种状态,因为真正的美元整合会有哪些?

1 个答案:

答案 0 :(得分:0)

正如你在问题中所说的那样

  

一个人告诉检查,断言传递给持久化方法的内容。

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

首先调用你的方法" setNotifyUsers"但它实际上做了2个动作,它调用整合对象上的setNotifyUsers,并保存/保存这些数据。 在我看来,这两种不同的行为属于两种不同的方法。如果您编写它可以帮助您进行测试:

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

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

您可以单独测试setNotifyUser和persistConsolidation,并为调用这些函数的部分编写功能测试(使用consolidationsService的方法) 然后你可以使用at()功能来查看是否以正确的顺序调用这些函数。

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

但是你不需要使用mockBuilder来进行双倍的测试。 您可以创建一个实际为您保存数据的FakeConsolidation,而不是使用$ this-> getMockBuilder

然后你还需要一个模拟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
  } 
}

现在因为你将一个对象提供给具有状态的持久化合作,我们可以在"中使用"来检查该状态。一部分。

当然,我不确切知道你的代码是如何构造的,所以我做了一些假设,只需根据需要进行调整,并使用你拥有的接口。

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

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移动到持久性

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

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