如何使用PHPUnit在Symfony2中设置数据库密集的单元测试?

k0p*_*kus 43 php phpunit symfony doctrine-orm

我对测试世界很陌生,我想确保我走在正确的轨道上.

我正在尝试使用phpunitsymfony2项目中设置单元测试.

PHPUnit正在运行,简单的默认控制器测试工作正常.(但这不是关于功能测试,而是单元测试我的应用程序.)

我的项目在很大程度上依赖于数据库交互,据我所知,从phpunit的文档中,我应该建立一个基于类的类\PHPUnit_Extensions_Database_TestCase,然后为我的数据库创建fixtures并从那里开始工作.

然而,symfony2只提供了一个WebTestCase只从\PHPUnit_Framework_TestCase开箱即用的类.

所以我是正确的,我认为我应该创建自己的DataBaseTestCase主要复制WebTestCase,唯一的区别是它扩展\PHPUnit_Extensions_Database_TestCase并实现其所有抽象方法?

或者是否有另一个"内置"推荐的symfony2工作流程涉及以数据库为中心的测试?

由于我想确保我的模型存储和检索正确的数据,我不想最终意外地测试学说的细节.

Sgo*_*kes 35

我从未使用过PHPUnit_Extensions_Database_TestCase,主要是因为这两个原因:

  • 它不能很好地扩展.如果您为每个测试设置并拆除数据库,并且您有一个严重依赖于数据库的应用程序,那么您最终会一次又一次地创建和删除相同的模式.
  • 我喜欢不仅在我的测试中,而且在我的开发数据库中都有我的灯具,甚至还需要一些灯具用于生产(初始管理员用户或产品类别或其他).将它们放在一个只能用于phpunit的xml中对我来说似乎不对.

我理论上的方式......

我使用doctrine/doctrine-fixtures-bundle作为灯具(无论用途),并使用所有灯具设置整个数据库.然后,我对该数据库执行所有测试,并确保在测试更改时重新创建数据库.

优点是,如果测试仅读取但不更改任何内容,则无需再次设置数据库.对于我必须做的更改,请删除它并再次创建它或确保还原更改.

我使用sqlite进行测试,因为我可以设置数据库,然后复制sqlite文件并用干净的文件替换它以恢复原始数据库.这样我就不必丢弃数据库,创建它并再次加载所有夹具以获得干净的数据库.

......并且在代码中

我写了一篇关于如何使用symfony2和phpunit进行数据库测试文章.

虽然它使用sqlite我认为可以轻松地进行更改以使用MySQL或Postgres或其他任何东西.

进一步思考

以下是一些可能有用的其他想法:

  • 我曾经读过一个测试设置,在你使用数据库之前,你启动一个事务(在setUp方法中),然后使用tearDown进行回滚.这样您就不需要再次设置数据库,只需要初始化一次.
  • 我上面描述的设置有一个缺点,即每次执行phpunit时都会设置数据库,即使你只运行一些没有数据库交互的单元测试.我正在尝试一个设置,其中我使用一个全局变量来指示数据库是否已设置,然后在测试中调用一个方法来检查此变量并初始化数据库(如果尚未发生).这样,只有当测试需要数据库时才会进行设置.
  • sqlite的一个问题是在某些极少数情况下它与MySQL的工作方式不同.我有一个问题,一旦MySQL和sqlite中的某些行为表现不同,导致测试失败,当使用MySQL时一切正常.我不记得究竟是什么.

  • 有一个捆绑包提供了PHPUnit和Doctrine的夹具之间的进一步集成,称为[Liip/FunctionalTestBundle](https://github.com/liip/LiipFunctionalTestBundle).我们将它与我们的config_test.yml配置中定义的sqlite数据库结合使用,它有助于简化很多事情!该捆绑包的一个非常有用的功能是它可以缓存您的sqlite数据库,因此数据库不会在测试之间共享,从而减少了为每个测试创建和设置数据库所涉及的开销. (7认同)

k0p*_*kus 3

长话短说:


我最初的问题中的某些方面清楚地表明我缺乏对单元测试和功能测试之间差异的理解。(正如我所写的,我想对应用程序进行单元测试,但同时也在谈论控制器测试;根据定义,这些是功能测试)。

单元测试仅对服务有意义,对存储库无效。这些服务可以使用实体管理器的模拟。(我什至会说:如果可能的话,编写只期望将实体传递给它们的服务。然后您只需要创建这些实体的模拟,并且业务逻辑的单元测试变得非常简单。)

我的应用程序的实际用例很好地反映在有关如何测试与数据库交互的代码的symfony2 文档中。

他们提供了这个示例来进行服务测试:

服务等级:

use Doctrine\Common\Persistence\ObjectManager;

class SalaryCalculator
{
    private $entityManager;

    public function __construct(ObjectManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function calculateTotalSalary($id)
    {
        $employeeRepository = $this->entityManager
            ->getRepository('AppBundle:Employee');
        $employee = $employeeRepository->find($id);

        return $employee->getSalary() + $employee->getBonus();
    }
}
Run Code Online (Sandbox Code Playgroud)

服务测试类:

namespace Tests\AppBundle\Salary;

use AppBundle\Salary\SalaryCalculator;
use AppBundle\Entity\Employee;
use Doctrine\ORM\EntityRepository;
use Doctrine\Common\Persistence\ObjectManager;

class SalaryCalculatorTest extends \PHPUnit_Framework_TestCase
{
    public function testCalculateTotalSalary()
    {
        // First, mock the object to be used in the test
        $employee = $this->getMock(Employee::class);
        $employee->expects($this->once())
            ->method('getSalary')
            ->will($this->returnValue(1000));
        $employee->expects($this->once())
            ->method('getBonus')
            ->will($this->returnValue(1100));

        // Now, mock the repository so it returns the mock of the employee
        $employeeRepository = $this
            ->getMockBuilder(EntityRepository::class)
            ->disableOriginalConstructor()
            ->getMock();
        $employeeRepository->expects($this->once())
            ->method('find')
            ->will($this->returnValue($employee));

        // Last, mock the EntityManager to return the mock of the repository
        $entityManager = $this
            ->getMockBuilder(ObjectManager::class)
            ->disableOriginalConstructor()
            ->getMock();
        $entityManager->expects($this->once())
            ->method('getRepository')
            ->will($this->returnValue($employeeRepository));

        $salaryCalculator = new SalaryCalculator($entityManager);
        $this->assertEquals(2100, $salaryCalculator->calculateTotalSalary(1));
    }
}
Run Code Online (Sandbox Code Playgroud)

此类测试不需要测试数据库,只需模拟。

因为测试业务逻辑很重要,而不是持久层。

只有对于功能测试来说,拥有自己的测试数据库才有意义,之后应该构建和拆除,最大的问题应该是:

功能测试什么时候有意义?

我曾经认为测试所有的事情就是正确的答案;然而,在使用了许多本身几乎没有测试驱动开发的遗留软件之后,我变得更加懒惰务实,并认为某些功能是有效的,直到被错误证明为止。

假设我有一个应用程序,它解析 XML、从中创建一个对象,并将这些对象存储到数据库中。如果已知将对象存储到数据库的逻辑可以工作(例如:公司需要数据,并且到目前为止还没有损坏),即使该逻辑是一大堆丑陋的垃圾,也没有迫切需要对此进行测试。我需要确保我的 XML 解析器提取正确的数据。我可以根据经验推断将存储正确的数据。

在某些情况下,功能测试非常重要,例如,如果要编写在线商店。在那里,将购买的商品存储到数据库中对于业务至关重要,并且在这里对整个测试数据库进行功能测试绝对有意义。