2016-02-05 64 views
0

我今天有一个相当奇怪的...Behat场景失败,如果与其他场景运行

我有一个Behat功能文件,其中包含几个方案。每个方案将通过如个别运行,但是如果我运行的特征文件在它的全部,则测试失败的一个,与错误...

Notice: Undefined index: 00000000070885f90000000106598262 in /project/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php line 2058 

...(如下图所示)

这些场景会加载灯具,然后使用灯具实体中的信息在页面周围导航,然后检查页面URL是否正确。

奇怪的是,第二个测试失败了,只有在第一个测试运行之前。如果不是,则上下文管理成功地将fixture实体存储为属性,然后使用EntityManager :: merge()和EntityManager :: refresh()将实体重新加载到它的当前状态。当第一个测试在它之前运行时,上下文仍然以相同的方式获取和存储fixture实体,但是当它尝试合并和刷新时,出于某种原因,实体管理器工作单元似乎已经忘记了它。

在每个场景之前,使用下面显示的代码清除d/b,重新装载夹具。我也确定我调用了EntityManager :: clear()来确保先前测试的所有残留都被删除。

/** 
* Clears the d/b 
* 
* @throws ToolsException 
*/ 
public function clearDb() 
{ 
    foreach ($this->getEntityManagers() as $entityManager) { 
     $metadata = $this->getMetadata($entityManager); 
     if (!empty($metadata)) { 
      $tool = new SchemaTool($entityManager); 
      $tool->dropSchema($metadata); 
      $tool->createSchema($metadata); 
     } 
    } 
} 

从我的调查多一点信息...

已经进一步调查,这不是一个问题,如果第一个测试只是取的实体,将其存储,但不会请求一个页面(使用水貂)

文件...

贝哈特测试(含注解)

@fix:Application\Stage9Submitted\SubmittedStage1 @fix:User\FundAdmin\FundAdmin1 
Scenario: I can assign an application to a case worker 
    Given I am logged in as "User\FundAdmin\FundAdmin1" fixture user 
    And I am on the application admin "eligibility" page for "Application\Stage9Submitted\SubmittedStage1" fixture application 
     ^== fetches amd saves as $currentEntity 
    And I should see "Unassigned" in the ".application-summary .case-worker" element 
    When I follow "Change case worker" 
    And I select "[email protected]" from "project_application_admin_change_caseworker_caseWorker" 
    And I press "Change case worker" 
    Then I should be on the application admin "eligibility" page for that application 
     ^== Retrieves $currentEntity and calls EntityManager::merge() and EntityManager::refresh() 
     ^== This works 

    And I should see "Fund Admin" in the ".application-summary .case-worker span[title='[email protected]']" element 
    And I should see "Application assigned to Fund Admin" 


@fix:Application\Stage9Submitted\SubmittedStage1 @fix:User\FundAdmin\FundAdmin1 
Scenario: I can un-assign an application from a case worker 
    Given I am logged in as "User\FundAdmin\FundAdmin1" fixture user 
    And I am on the application admin "eligibility" page for "Application\Stage9Submitted\SubmittedStage1" fixture application 
     ^== fetches amd saves as $currentEntity 
    And I should see "Unassigned" in the ".application-summary .case-worker" element 
    And I follow "Change case worker" 
    And I select "[email protected]" from "project_application_admin_change_caseworker_caseWorker" 
    And I press "Change case worker" 
    And I should be on the application admin "eligibility" page for that application 
     ^== Retrieves $currentEntity and calls EntityManager::merge() and EntityManager::refresh() 
     ^== This fails (but only if the above test run at the same time!?!) 
    ... 

FixturesContext

<?php 

namespace CubicMushroom\SymfonyFeatureContextBundle\Feature\Context; 

use Behat\Behat\Hook\Scope\BeforeScenarioScope; 
use Behat\Symfony2Extension\Context\KernelAwareContext; 
use Doctrine\Common\DataFixtures\AbstractFixture; 
use Doctrine\Common\DataFixtures\Executor\ORMExecutor; 
use Doctrine\Common\DataFixtures\FixtureInterface; 
use Doctrine\Common\DataFixtures\Loader; 
use Doctrine\Common\DataFixtures\Purger\ORMPurger; 
use Doctrine\Common\DataFixtures\ReferenceRepository; 
use Doctrine\DBAL\Connection; 
use Doctrine\ORM\EntityManager; 
use Doctrine\ORM\Tools\SchemaTool; 
use Doctrine\ORM\Tools\ToolsException; 
use Project\DataFixtures\ORM\AbstractSingleFixture; 
use Project\Exception\Feature\Context\FixtureContext\FixtureNotFoundException; 
use Symfony\Component\DependencyInjection\ContainerInterface; 
use Symfony\Component\HttpKernel\KernelInterface; 

/** 
* Loads fixtures based on scenario tags 
* 
* @package Project 
*/ 
class FixturesContext implements KernelAwareContext 
{ 
    // ----------------------------------------------------------------------------------------------------------------- 
    // Properties 
    // ----------------------------------------------------------------------------------------------------------------- 

    /** 
    * @var KernelInterface 
    */ 
    protected $kernel; 

    /** 
    * @var array 
    */ 
    protected $fixtureNamespaces; 

    /** 
    * @var Loader 
    */ 
    protected $loader; 

    /** 
    * @var AbstractFixture[] 
    */ 
    protected $loadedFixtures; 

    /** 
    * @var ORMExecutor 
    */ 
    protected $executor; 


    /** 
    * NewFixturesContext constructor. 
    * 
    * @param array $fixtureNamespaces 
    */ 
    public function __construct(array $fixtureNamespaces) 
    { 
     foreach ($fixtureNamespaces as $fixtureNamespace) { 
      $this->addFixtureNamespace($fixtureNamespace); 
     } 
    } 


    // ----------------------------------------------------------------------------------------------------------------- 
    // @BeforeScenario 
    // ----------------------------------------------------------------------------------------------------------------- 

    /** 
    * @BeforeScenario 
    * 
    * @param BeforeScenarioScope $scope 
    */ 
    public function loadFixturesFromTags(BeforeScenarioScope $scope) 
    { 
     // We load this here, rather than in the constructor so it's re-initialised on each scenario 
     $this->loader = new Loader(); 

     $tags = $scope->getScenario()->getTags(); 

     foreach ($tags as $tag) { 
      $this->loadFixturesForTag($this->loader, $tag); 
     } 

     $fixtures = $this->loader->getFixtures(); 

     if (empty($fixtures)) { 
      return; 
     } 

     $this->clearDb(); 

     $em = $this->getEntityManager(); 
     $em->clear(); 

     $purger   = new ORMPurger(); 
     $this->executor = new ORMExecutor($em, $purger); 
     $this->executor->purge(); 
     $this->executor->execute($fixtures, true); 

     $this->loadedFixtures = $fixtures; 
    } 


    /** 
    * @param string $fixture 
    * 
    * @return array 
    */ 
    public function getNamespacedFixtures($fixture) 
    { 
     $fixtures = []; 

     foreach ($this->fixtureNamespaces as $fixtureNamespace) { 

      $fixtureClass = "{$fixtureNamespace}\\{$fixture}"; 

      if (class_exists($fixtureClass)) { 
       $fixtures[] = $fixtureClass; 
      } 
     } 

     return $fixtures; 
    } 


    /** 
    * Clears the d/b 
    * 
    * @throws ToolsException 
    */ 
    public function clearDb() 
    { 
     foreach ($this->getEntityManagers() as $entityManager) { 
      $metadata = $this->getMetadata($entityManager); 
      if (!empty($metadata)) { 
       $tool = new SchemaTool($entityManager); 
       $tool->dropSchema($metadata); 
       $tool->createSchema($metadata); 
      } 
     } 
    } 


    /** 
    * Loads the fixtures for a given tag 
    * 
    * @param Loader $loader 
    * @param string $tag 
    */ 
    protected function loadFixturesForTag(Loader $loader, $tag) 
    { 
     $parts = explode(':', $tag); 
     $prefix = array_shift($parts); 

     // Only bother with tags staring 'fix:' 
     if ('fix' !== $prefix) { 
      return; 
     } 

     if (empty($parts)) { 
      throw new \LogicException('No fixture provided'); 
     } 

     $fixture = array_shift($parts); 
     $args = $parts; 

     $fixtureClasses = $this->getNamespacedFixtures($fixture); 

     foreach ($fixtureClasses as $fixtureClass) { 
      $reflect = new \ReflectionClass($fixtureClass); 
      $instance = $reflect->newInstanceArgs($args); 

      if (!$instance instanceof FixtureInterface) { 
       throw new \InvalidArgumentException("Class {$fixtureClass} does not implement FixtureInterface"); 
      } 

      $loader->addFixture($instance); 

      return; 
     } 

     throw FixtureNotFoundException::create($fixture); 
    } 


    /** 
    * @AfterScenario 
    * 
    * 
    * @return null 
    */ 
    public function closeDBALConnections() 
    { 
     /** @var EntityManager $entityManager */ 
     foreach ($this->getEntityManagers() as $entityManager) { 
      $entityManager->clear(); 
     } 
     /** @var Connection $connection */ 
     foreach ($this->getConnections() as $connection) { 
      $connection->close(); 
     } 
    } 


    // ----------------------------------------------------------------------------------------------------------------- 
    // Getters and Setters 
    // ----------------------------------------------------------------------------------------------------------------- 


    /** 
    * @param $fixturesDir 
    * 
    * @return $this 
    */ 
    protected function addFixtureNamespace($fixturesDir) 
    { 
     if (!isset($this->fixtureNamespaces)) { 
      $this->fixtureNamespaces = []; 
     } 

     if (!in_array($fixturesDir, $this->fixtureNamespaces)) { 
      $this->fixtureNamespaces[] = $fixturesDir; 
     } 

     return $this; 
    } 


    /** 
    * Sets Kernel instance. 
    * 
    * @param KernelInterface $kernel 
    */ 
    public function setKernel(KernelInterface $kernel) 
    { 
     $this->kernel = $kernel; 
    } 


    /** 
    * @return ContainerInterface 
    */ 
    protected function getContainer() 
    { 
     return $this->kernel->getContainer(); 
    } 


    /** 
    * @param EntityManager $entityManager 
    * 
    * @return array 
    */ 
    protected function getMetadata(EntityManager $entityManager) 
    { 
     return $entityManager->getMetadataFactory()->getAllMetadata(); 
    } 


    /** 
    * @return array 
    */ 
    protected function getEntityManagers() 
    { 
     return $this->getContainer()->get('doctrine')->getManagers(); 
    } 


    /** 
    * @return EntityManager 
    */ 
    protected function getEntityManager() 
    { 
     $em = $this->kernel->getContainer()->get('doctrine.orm.entity_manager'); 

     return $em; 
    } 


    /** 
    * @return Connection[] 
    */ 
    protected function getConnections() 
    { 
     return $this->kernel->getContainer()->get('doctrine')->getConnections(); 
    } 


    /** 
    * @return ORMExecutor 
    */ 
    public function getExecutor() 
    { 
     return $this->executor; 
    } 


    /** 
    * @return ReferenceRepository 
    */ 
    public function getReferenceRepository() 
    { 
     return $this->executor->getReferenceRepository(); 
    } 


    /** 
    * @param string $fixtureClass 
    * 
    * @return FixtureInterface 
    * 
    * @throws \OutOfBoundsException if fixture not found 
    */ 
    public function getFixture($fixtureClass) 
    { 
     try { 
      $userFixture = $this->_getFixture($fixtureClass); 
     } catch (\OutOfBoundsException $exception) { 
      $fixtures = $this->getNamespacedFixtures($fixtureClass); 

      if (empty($fixtures)) { 
       throw new \OutOfBoundsException("Fixture {$fixtureClass} not found"); 
      } 

      if (count($fixtures) > 1) { 
       throw new \LogicException(
        "Found multiple {$fixtureClass} fixtures. Use the full namespace to correct" 
       ); 
      } 

      /** @var AbstractSingleFixture $userFixture */ 
      $userFixture = $this->_getFixture($fixtures[0]); 
     } 

     return $userFixture; 
    } 


    /** 
    * @param string $fixtureClass 
    * 
    * @return FixtureInterface 
    * 
    * @throws \OutOfBoundsException if fixture not found 
    */ 
    protected function _getFixture($fixtureClass) 
    { 
     foreach ($this->loader->getFixtures() as $fixture) { 
      if (is_a($fixture, $fixtureClass)) { 
       return $fixture; 
      } 
     } 

     throw new \OutOfBoundsException("Fixture '{$fixtureClass}' not found'"); 
    } 


    /** 
    * @param $fixtureClass 
    * 
    * @return object 
    * 
    * @throw \OutOfBoundsException if fixture is not found 
    */ 
    public function getFixtureEntity($fixtureClass) 
    { 
     // Fixture class could be a shorthand, without namespace, so we use getFixture to get the full class name… 
     $fixture  = $this->getFixture($fixtureClass); 
     $fixtureClass = get_class($fixture); 

     $referenceRepository = $this->getReferenceRepository(); 

     if (!$referenceRepository->hasReference($fixtureClass)) { 
      throw new \OutOfBoundsException("Fixture '{$fixtureClass}' not found"); 
     } 

     return $referenceRepository->getReference($fixtureClass); 
    } 
} 

UnitOfWork.php

# /project/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php showing line 2058 
# (marked on RH side of code) 

<?php 

namespace Doctrine\ORM; 

use ... 

class UnitOfWork implements PropertyChangedListener 
{ 

    // ... 

    /** 
    * Executes a refresh operation on an entity. 
    * 
    * @param object $entity The entity to refresh. 
    * @param array $visited The already visited entities during cascades. 
    * 
    * @return void 
    * 
    * @throws ORMInvalidArgumentException If the entity is not MANAGED. 
    */ 
    private function doRefresh($entity, array &$visited) 
    { 
     $oid = spl_object_hash($entity); 

     if (isset($visited[$oid])) { 
      return; // Prevent infinite recursion 
     } 

     $visited[$oid] = $entity; // mark visited 

     $class = $this->em->getClassMetadata(get_class($entity)); 

     if ($this->getEntityState($entity) !== self::STATE_MANAGED) { 
      throw ORMInvalidArgumentException::entityNotManaged($entity); 
     } 

     $this->getEntityPersister($class->name)->refresh(
      array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),  <===== Line 2058 
      $entity 
     ); 

     $this->cascadeRefresh($entity, $visited); 
    } 

    // ... 
} 

我不会在这里发布我所有的上下文类,但如果你需要更多的信息,请让我知道。

任何帮助或指针与此将不胜感激。

非常感谢。

+0

如果您正在提取实体,为什么还要融合并刷新它?请告诉我们情况。 –

+0

@JakubZalas - 因为在测试过程中数据可能已经发生变化,所以需要检查最新的版本正在被考虑(虽然不在这些测试中) – TobyG

+0

您在测试环境中使用了任何种类的原则缓存吗?如果是这样,请尝试禁用它。 – gvf

回答

0

可能您的场景不会被正确地“撕下”。

我必须对这个问题的解决方案:

1)SLOW APPROACH

重新创建DB数据(因此,负载灯具基本上)每次运行一个新的场景时间

2)更快的方法

在事务中运行每个场景并在每个场景完成后放弃所有更改

您的测试应该被隔离,并且不会受到在其之前或之后运行的其他测试的影响。

+0

感谢您的建议,但是我已经在每个场景之前清除d/b并重新加载灯具。 – TobyG

+0

@TobyG:你说“清理数据库”是什么意思?如果你分离实体或类似的东西,你不会清除任何东西:)或者,至少,更安全地遵循我的方法,相信我...我对这个奇怪的执行很有经验:) – DonCallisto

+0

我已经添加用于清除d/b到主要问题的代码 – TobyG