在Symfony 2 WebTestCase上创建测试数据库和加载fixture的最佳方法是什么?

时间:2013-02-07 14:00:56

标签: symfony doctrine tdd

我有一个WebTestCase,它在我的应用程序中执行一些基本路由。

我想在PHPUnit的setUp方法中创建一个与我的主数据库相同的测试数据库,并将数据加载到其中。

我正在做一些解决方法并执行一些控制台命令,如下所示:

class FixturesWebTestCase extends WebTestCase
{
    protected static $application;

    protected function setUp()
    {
        self::runCommand('doctrine:database:create');
        self::runCommand('doctrine:schema:update --force');
        self::runCommand('doctrine:fixtures:load --purge-with-truncate');
    }

    protected static function runCommand($command)
    {
        $command = sprintf('%s --quiet', $command);

        return self::getApplication()->run(new StringInput($command));
    }

    protected static function getApplication()
    {
        if (null === self::$application) {
            $client = static::createClient();

            self::$application = new Application($client->getKernel());
            self::$application->setAutoExit(false);
        }

        return self::$application;
    }
}

但我确信这不是最好的方法,特别是因为doctrine:fixtures:load期望用户点击Y字符来确认操作。

我该如何解决?

7 个答案:

答案 0 :(得分:34)

为了绕过用户确认,您可以使用

doctrine:fixtures:load --no-interaction
or
doctrine:fixtures:load -n

答案 1 :(得分:33)

如果您想使用doctrine:fixtures:load,可以使用--append选项来避免用户确认。由于每次都在重新创建数据库,因此不需要进行清除。我曾经单独使用学说装置进行测试,但后来转而使用灯具和放大器。 LiipFunctionalTestBundle以避免干旱。该捆绑包使夹具更易于管理。

编辑:David Jacquel的答案是加载Doctrine Fixtures的正确答案:

doctrine:fixtures:load --no-interaction 
or
doctrine:fixtures:load -n

答案 2 :(得分:31)

更新的答案

您可以为测试用例创建一个基类,通过利用Doctrine Data Fixtures库中的某些类,可以轻松加载夹具。这个类看起来很像这样:

<?php

use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;

abstract class FixtureAwareTestCase extends KernelTestCase
{
    /**
     * @var ORMExecutor
     */
    private $fixtureExecutor;

    /**
     * @var ContainerAwareLoader
     */
    private $fixtureLoader;

    public function setUp()
    {
        self::bootKernel();
    }

    /**
     * Adds a new fixture to be loaded.
     *
     * @param FixtureInterface $fixture
     */
    protected function addFixture(FixtureInterface $fixture)
    {
        $this->getFixtureLoader()->addFixture($fixture);
    }

    /**
     * Executes all the fixtures that have been loaded so far.
     */
    protected function executeFixtures()
    {
        $this->getFixtureExecutor()->execute($this->getFixtureLoader()->getFixtures());
    }

    /**
     * @return ORMExecutor
     */
    private function getFixtureExecutor()
    {
        if (!$this->fixtureExecutor) {
            /** @var \Doctrine\ORM\EntityManager $entityManager */
            $entityManager = self::$kernel->getContainer()->get('doctrine')->getManager();
            $this->fixtureExecutor = new ORMExecutor($entityManager, new ORMPurger($entityManager));
        }
        return $this->fixtureExecutor;
    }

    /**
     * @return ContainerAwareLoader
     */
    private function getFixtureLoader()
    {
        if (!$this->fixtureLoader) {
            $this->fixtureLoader = new ContainerAwareLoader(self::$kernel->getContainer());
        }
        return $this->fixtureLoader;
    }
}

然后,在您的测试用例中,只需扩展上面的类,然后在测试之前添加所有需要的灯具并执行它们。这将在加载灯具之前自动清除数据库。示例如下:

class MyTestCase extends FixtureAwareTestCase
{
    public function setUp()
    {
        parent::setUp();

        // Base fixture for all tests
        $this->addFixture(new FirstFixture());
        $this->addFixture(new SecondFixture());
        $this->executeFixtures();

        // Fixtures are now loaded in a clean DB. Yay!
    }
}

OLD ANSWER

(我决定“弃用”这个答案,因为它只解释了如何清理数据库而不告诉如何在之后加载灯具)。

有一种更简洁的方法来完成此操作而无需运行命令。它基本上包括使用SchemaTool和ORMPurger的组合。您可以创建一个抽象基类来执行此类操作,以避免为每个专门的测试用例重复它们。下面是一个测试用例类的代码示例,它为通用测试用例设置数据库:

use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Doctrine\ORM\Tools\SchemaTool;

abstract class DatabaseAwareWebTestCase extends WebTestCase {

    public static function setUpBeforeClass() {

        parent::setUpBeforeClass();

        $kernel = static::createKernel();
        $kernel->boot();
        $em = $kernel->getContainer()->get('doctrine')->getManager();
        $schemaTool = new SchemaTool($em);
        $metadata = $em->getMetadataFactory()->getAllMetadata();

        // Drop and recreate tables for all entities
        $schemaTool->dropSchema($metadata);
        $schemaTool->createSchema($metadata);
    }

    protected function tearDown() {

        parent::tearDown();

        $purger = new ORMPurger($this->getContainer()->get('doctrine')->getManager());
        $purger->setPurgeMode(ORMPurger::PURGE_MODE_TRUNCATE);
        $purger->purge();
    }
}

这样,在运行继承自上述类的每个测试用例之前,将从头开始重建数据库模式,然后在每次测试运行后进行清理。

希望这有帮助。

答案 3 :(得分:5)

我偶然发现了一个名为Doctrine-Test-Bundle的非常整洁的软件包 它不是在每个测试中创建和删除模式,而是简单地回滚。 我的测试从1分40秒到2分钟。它是孤立的。 您所需要的只是一个清晰的测试数据库,它可以解决问题。

答案 4 :(得分:2)

我使用了这个命令:

yes | php app/console doctrine:fixtures:load --purge-with-truncate

但是LiipFunctionalTestBundle当然看起来很有希望。

答案 5 :(得分:0)

我想加载像Sample Code: jsp code: <div id="cycle-slideshow" style="width: 100%;"> <div class="container-fluid animated" id="container_<%=i%>" style="opacity: 0.01;width: 100%;"> <div class="row"> <div class="container-fluid" id="container"> <div class="row border-bottom"> <nav class="navbar navbar-static-top white-bg main_header" role="navigation" style="margin-bottom: 0"> <div class="header" id="header"> <div class="col-lg-12" style="max-height:25px"> <div class="col-lg-6"> <h2 > <s:property value="name" escape="false"/> </h2> </div> <div class="col-lg-6"> <h4 class="pull-right push-bottom" style="margin-top:25px;"> <span class="timeData" style="text-align:center;"></span> </h4> </div> </div> </div> </nav> </div> <!-------------------header end------------------------> <!-------------------------graph and stream area----------------------------> <!----------------------left panel start--------------------> <div class="wrapper wrapper-content" style="padding: 6px 5px 20px !important;"> <div class="row"> <div class="col-lg-9" id="graph_area"> <div class="row_custom"> <div class="row"> <div class="col-lg-12"> <div class="ibox float-e-margins" style="margin-bottom:14px;"> <div class="ibox-title graph_header"> <h5><s:property value="settings[1].elementName" escape="false"/></h5> </div> <div class="ibox-content" style="padding-bottom:4px;"> <div class="row"> <div class="col-lg-12" style="padding:4px;" id="<s:property value="settings[1].elementId"/>_<s:property value="settings[1].id"/>_<s:property value="id"/>" > </div> </div> </div> </div> </div> </div> <div class="row" > <div class="col-lg-6"> <div class="row" style="margin-left:1px!important;"> <div class="ibox float-e-margins"> <div class="ibox-title graph_header"> <h5><s:property value="settings[3].elementName"/></h5> </div> <div class="ibox-content" style="padding-bottom:4px;"> <div class="row" > <div class="col-lg-12" id="<s:property value="settings[3].elementId"/>_<s:property value="settings[3].id"/>_<s:property value="id"/>" style="display:inline-block;"> </div> </div> </div> </div> </div> </div> <div class="col-lg-6"> <div class="ibox float-e-margins"> <div class="ibox-title graph_header"> <h5><s:property value="settings[4].elementName"/></h5> </div> <div class="ibox-content" style="padding-bottom:14px;"> <div class="row"> <div class="col-lg-12" style="padding:4px;display:inline-block;" id="<s:property value="settings[4].elementId"/>_<s:property value="settings[4].id"/>_<s:property value="id"/>" > //area for map </div> </div> </div> </div> </div> </div> </div> </div> <!----------------------left panel End--------------------> <!----------------------right panel start--------------------> <div class="col-lg-3" id="data_stream_div" > <div class="row"> <div class="ibox float-e-margins"> <div class="ibox-title graph_header"> <h5><s:property value="settings[2].elementName"/></h5> </div> <div class="ibox-content" id="temp_1_stream" style="padding-left:1px!important;"> <div> <div class="feed-activity-list" style="padding:4px;" id="<s:property value="settings[2].elementId"/>_<s:property value="settings[2].id"/>_<s:property value="id"/>" > //area for table </div> </div> </div> </div> </div> </div> <!----------------------right panel End----------------------> </div> </div> <!---------------------graph area------------------> </div> </div> </div> <div class="container-fluid animated" id="container_<%=i%>" style="opacity: 0.01;width: 100%;"> <div class="row"> <div class="container-fluid" id="container"> <div class="row border-bottom"> <nav class="navbar navbar-static-top white-bg main_header" role="navigation" style="margin-bottom: 0"> <div class="header" id="header"> <div class="col-lg-12" style="max-height:25px"> <div class="col-lg-6"> <h2 > <s:property value="name" escape="false"/> </h2> </div> <div class="col-lg-6"> <h4 class="pull-right push-bottom" style="margin-top:25px;"> <span class="timeData" style="text-align:center;"></span> </h4> </div> </div> </div> </nav> </div> <!-------------------header end------------------------> <!-------------------------graph and stream area----------------------------> <!----------------------left panel start--------------------> <div class="wrapper wrapper-content" style="padding: 6px 5px 20px !important;"> <div class="row"> <div class="col-lg-9" id="graph_area"> <div class="row_custom"> <div class="row"> <div class="col-lg-12"> <div class="ibox float-e-margins" style="margin-bottom:14px;"> <div class="ibox-title graph_header"> <h5><s:property value="settings[1].elementName" escape="false"/></h5> </div> <div class="ibox-content" style="padding-bottom:4px;"> <div class="row"> <div class="col-lg-12" style="padding:4px;" id="<s:property value="settings[1].elementId"/>_<s:property value="settings[1].id"/>_<s:property value="id"/>" > </div> </div> </div> </div> </div> </div> <div class="row" > <div class="col-lg-6"> <div class="row" style="margin-left:1px!important;"> <div class="ibox float-e-margins"> <div class="ibox-title graph_header"> <h5><s:property value="settings[3].elementName"/></h5> </div> <div class="ibox-content" style="padding-bottom:4px;"> <div class="row" > <div class="col-lg-12" id="<s:property value="settings[3].elementId"/>_<s:property value="settings[3].id"/>_<s:property value="id"/>" style="display:inline-block;"> </div> </div> </div> </div> </div> </div> <div class="col-lg-6"> <div class="ibox float-e-margins"> <div class="ibox-title graph_header"> <h5><s:property value="settings[4].elementName"/></h5> </div> <div class="ibox-content" style="padding-bottom:14px;"> <div class="row"> <div class="col-lg-12" style="padding:4px;display:inline-block;" id="<s:property value="settings[4].elementId"/>_<s:property value="settings[4].id"/>_<s:property value="id"/>" > //area for piechart </div> </div> </div> </div> </div> </div> </div> </div> <!----------------------left panel End--------------------> <!----------------------right panel start--------------------> <div class="col-lg-3" id="data_stream_div" > <div class="row"> <div class="ibox float-e-margins"> <div class="ibox-title graph_header"> <h5><s:property value="settings[2].elementName"/></h5> </div> <div class="ibox-content" id="temp_1_stream" style="padding-left:1px!important;"> <div> <div class="feed-activity-list" style="padding:4px;" id="<s:property value="settings[2].elementId"/>_<s:property value="settings[2].id"/>_<s:property value="id"/>" > //area for table </div> </div> </div> </div> </div> </div> <!----------------------right panel End----------------------> </div> </div> <!---------------------graph area------------------> </div> </div> </div> </div> code for slider: function getslider(){ $('#cycle-slideshow').cycle({ timeout: 90000, slides: '> div', fx:'fade' }); $( '#cycle-slideshow' ).on( 'cycle-after', function( event, opts ) { console.log("lastRunTime "+lastRunTime); console.log("timedifference "+((lastRunTime-event.timeStamp)/1000)); lastRunTime=event.timeStamp; }); } code for piechart: function getShareOfVoice() { $.ajax({ type:"POST", dataType:"json", url:'<s:url value="XXXXXX', data:{sampledata : sampledate}, cache: false, async:"false", success:function(data) { console.log("in success for share of voice"); var vals = JSON.parse(JSON.stringify(data)); if(vals.data.length>0){ var dataArray = vals.data; var overallseries = []; overallShareOfVoiceOptions.chart.renderTo = divid; for ( var pos = 0; pos < dataArray.length; pos++) { var d = dataArray[pos]; var dd = {}; var legend_icon = " "; if (d.pic != null && typeof d.pic !== "undefined" && d.pic.length > 0) { legend_icon = '<img style="width: 45px; height: 30px" src="'+d.pic+'" >'; } else { legend_icon = d.name; } dd.color = d.cssStyle; dd.y = d.count; dd.name = legend_icon; overallseries.push(dd); } overallShareOfVoiceOptions.series[0].data = overallseries; if (overallseries.length > 0) { var shareOfVoiceOverallGraph = new Highcharts.Chart( overallShareOfVoiceOptions); } if (draw !== true) { progressbar(divid); } setTimeout(function() { getShareOfVoice(); }, 5 * 60 * 1000); } },error:function(data){ setTimeout(function() { getShareOfVoice(); }, 2 * 60 * 1000); } }); } code for highMap: function getRegionChart() { $.ajax({ type:"POST", url:'XXXXXXXXXXXXXXXX', dataType:'json', data : {sampledata : sampledata}, success : function(data){ var regions = JSON.parse(JSON.stringify(data)); if(regions.length > 0){ $('#' + divid).highcharts('Map',{ title : { text : null }, subtitle : { text : null }, mapNavigation : { enabled : false, buttonOptions : { verticalAlign : 'bottom' } }, legend : { align : 'left', layout : 'vertical', verticalAlign : 'middle', y : 10, floating : true, borderWidth : 0, backgroundColor : 'white', }, colorAxis : { min : 0, minColor : '#cccc00', maxColor : '#666600' }, navigation : { menuItemStyle : { fontSize : '10px' }, buttonOptions : { enabled : false } }, credits : { enabled : false }, series : [ { data : regions, mapData : Highcharts.maps['countries/in/custom/in-all-disputed'], joinBy : 'hc-key', name : 'State', states : { hover : { color : '#BADA55' } }, dataLabels : { enabled : true, formatter : function() { if(!(this.point.options["show"])) return; return this.point.value+ "%"; } } } ] }); setTimeout(function() { getRegionChart(); }, 5*60 *1000); } },error : function(data) { setTimeout(function() { getRegionChart(); },2*60 * 1000); } }); } 命令那样的所有灯具。我不想从测试用例中运行doctrine:fixtures:load,因为它似乎是一种乱糟糟的做事方式。我看了一下doctrine命令是如何做到的,只是复制了相关的行。

我从Symfony exec扩展而且在创建内核之后,我只调用了我的方法,它的工作方式与Doctrine load-fixtures命令完全相同。

WebTestCase

答案 6 :(得分:0)

最近,捆绑软件hautelook/AliceBundle暴露了两个特征,可帮助您解决功能测试中加载夹具的用例:RefreshDatabaseTraitReloadDatabaseTrait

来自文档:

namespace App\Tests;

use Hautelook\AliceBundle\PhpUnit\RefreshDatabaseTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class NewsTest extends WebTestCase
{
    use RefreshDatabaseTrait;

    public function postCommentTest()
    {
        $client = static::createClient(); // The transaction starts just after the boot of the Symfony kernel
        $crawler = $client->request('GET', '/my-news');
        $form = $crawler->filter('#post-comment')->form(['new-comment' => 'Symfony is so cool!']);
        $client->submit($form);
        // At the end of this test, the transaction will be rolled back (even if the test fails)
    }
}

你很好!