首页 文章

如何在没有任何实体的情况下测试doctrine EventListener / Subscriber

提问于
浏览
3

我创建了一个AuditLoggerBundle *,它有一个使用Doctrine Events(prePersist,preUpdate和preRemove)的服务,以便在audit_log表(AuditLog Entity)中创建一个新条目 .

该捆绑包与我的其他捆绑包工作正常,但我想 unit test it and functional test it .

问题是,为了对 AuditLoggerListener 函数进行功能测试,我需要至少有两个"fake"实体,我可以坚持,更新等 .

在这个包中我不知道如何做到这一点,因为我只有一个AuditLog实体,我需要使用两个over实体( that will be only used in tests ) .

  • 第一个实体将是"auditable"(如果我在此实体上执行持久化,更新或删除,我必须在audit_log中有一个新条目) .

  • 第二个将是"non-auditable"(当我在此实体上执行持久化,更新或删除时,我不能在audit_log表中有新条目) . *

  • 这两个实体可以与唯一的EntityClass相关,但不能是AuditLog的实例

这就是我看到持续功能测试的方式:

<?php
$animal = new Animal(); //this is a fake Auditable entity
$animal->setName('toto');
$em = new EntityManager(); //actually I will use the container to get this manager
$em->persist($animal);
$em->flush();

//Here we test that I have a new line in audit_log table with the right informations

所以我的问题是我的捆绑包中没有任何Animal实体,我只需要这个来测试捆绑包,所以它必须只在测试数据库中创建,而不是在 生产环境 环境中创建(当我做 app/console doctrine:schema:update --force 时)

EDIT_1: After reading your answers, Unit Tests on AuditLoggerListener functions are going to be performed but I still want to make functional tests

*是的我知道它们有很多,但它们并不符合我的要求 .

感谢您的回答,我希望它能帮助一些人!

EDIT_2: here is the code 服务:

services:
    #add a prefix to the auditLogger table
    kali_audit_logger.doctrine.table.prefix:
        class: Kali\AuditLoggerBundle\EventListener\TablePrefixListener
        arguments: [%application.db.table.prefix%]
        tags:
            - { name: doctrine.event_listener, event: loadClassMetadata }

    #audit all doctrine actions made by a user
    kali_audit_logger.doctrine.event.logger:
        class: Kali\AuditLoggerBundle\EventListener\AuditLoggerListener
        arguments: [@kali_audit_log, @jms_serializer.serializer, @security.token_storage, %application.auditable.entities%, %application.non.auditable.entities%]
        tags:
            - { name: doctrine.event_listener, event: prePersist }
            - { name: doctrine.event_listener, event: preUpdate }
            - { name: doctrine.event_listener, event: preRemove }

    # new AuditLog
    kali_audit_log:
        class: Kali\AuditLoggerBundle\Entity\AuditLog

监听器:

namespace Kali\AuditLoggerBundle\EventListener;

use DateTime;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use JMS\Serializer\SerializerInterface;
use Kali\AuditLoggerBundle\Entity\AuditLog;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Serializer\Encoder\JsonEncoder;

/**
 * Class AuditLoggerListener
 * insert a new entry in audit_log table for every doctrine event
 *
 * @package Kali\AuditLoggerBundle\EventListener
 */
class AuditLoggerListener
{
    /**
     * @var TokenStorage
     */
    protected $securityToken;

    /**
     * @var EntityManager
     */
    protected $em;

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

    /**
     * @var array
     */
    protected $nonAuditableEntities  = ['Kali\AuditLoggerBundle\Entity\AuditLog'];

    /**
     * @var AuditLog
     */
    protected $auditLogger;

    /**
     * @var SerializerInterface
     */
    protected $serializer;

    /**
     * @param AuditLog $auditLogger
     * @param SerializerInterface $serializer
     * @param TokenStorage $securityToken
     * @param array $auditableEntities
     * @param array $nonAuditableEntities
     */
    public function __construct(
        AuditLog $auditLogger,
        SerializerInterface $serializer,
        TokenStorage $securityToken,
        $auditableEntities = [],
        $nonAuditableEntities = []
    ) {
        $this->auditLogger          =   $auditLogger;
        $this->serializer           =   $serializer;
        $this->securityToken        =   $securityToken;
        $this->auditableEntities    =   $auditableEntities;
        //add all non auditable entities to the current array of non auditable entities
        array_merge($this->nonAuditableEntities, $nonAuditableEntities);
    }

    /**
     *
     * @param LifecycleEventArgs $args
     *
     * @return boolean
     */
    public function prePersist(LifecycleEventArgs $args)
    {
        $this->em   =   $args->getEntityManager();
        $entity     =   $args->getEntity();

        $this->em
            ->getEventManager()
            ->removeEventListener('prePersist', $this);

        if ($this->isAuditableEntity($entity)) {
            $this->addAudit(
                $this->securityToken->getToken()->getUsername(),
                "INSERT",
                get_class($entity),
                $this->serializer->serialize($entity, JsonEncoder::FORMAT)
            );
        }

        return true;
    }

    /**
     *
     * @param PreUpdateEventArgs $args
     *
     * @return boolean
     */
    public function preUpdate(PreUpdateEventArgs $args)
    {
        $this->em   =   $args->getEntityManager();
        $entity     =   $args->getEntity();

        $this->em
            ->getEventManager()
            ->removeEventListener('preUpdate', $this);

        if ($this->isAuditableEntity($entity)) {
            $this->addAudit(
                $this->securityToken->getToken()->getUsername(),
                "UPDATE",
                get_class($entity),
                $this->serializer->serialize($entity, JsonEncoder::FORMAT),
                $this->serializer->serialize($args->getEntityChangeSet(), JsonEncoder::FORMAT)
            );
        }

        return true;
    }

    /**
     *
     * @param LifecycleEventArgs $args
     *
     * @return boolean
     */
    public function preRemove(LifecycleEventArgs $args)
    {
        $this->em   =   $args->getEntityManager();
        $entity     =   $args->getEntity();

        $this->em
            ->getEventManager()
            ->removeEventListener('preRemove', $this);

        if ($this->isAuditableEntity($entity)) {
            $this->addAudit(
                $this->securityToken->getToken()->getUsername(),
                "REMOVE",
                get_class($entity),
                $this->serializer->serialize($entity, JsonEncoder::FORMAT)
            );
        }

        return true;
    }

    /**
     * Insert a new line in audit_log table
     *
     * @param string      $user
     * @param string      $action
     * @param string      $entityClass
     * @param null|string $entityValue
     * @param null|string $entityChange
     *
     * @return void
     */
    private function addAudit($user, $action, $entityClass, $entityValue = null, $entityChange = null)
    {
        if ($this->auditLogger) {
            $this->auditLogger
                ->setUser($user)
                ->setAction($action)
                ->setEntityClass($entityClass)
                ->setEntityValue($entityValue)
                ->setEntityChange($entityChange)
                ->setDate(new DateTime());
        }

        if ($this->em) {
            $this->em->persist($this->auditLogger);
            $this->em->flush();
        }
    }

    /**
     * check if an entity is auditable
     *
     * @param $entity
     *
     * @return bool
     */
    private function isAuditableEntity($entity)
    {
        $auditable = false;

        //the entity must not be in the non auditable entity array
        if (!in_array(get_class($entity), $this->nonAuditableEntities)
            && (empty($this->auditableEntities) || (!empty($this->auditableEntities) && in_array(get_class($entity), $this->auditableEntities)))
        ) {
            $auditable = true;
        }

        return $auditable;
    }
}

我想测试这个监听器的preXXXX函数......所以,例如,我需要测试当我在 fake entity (我真的不知道如何模拟)上执行持久化时,有一个新的条目我的audit_log表...

2 回答

  • 3

    测试php类的单元意味着只测试此类包含的代码而无需任何外部交互 . 所以你应该模拟所有外部服务:参见phpunit mock documentation https://phpunit.de/manual/current/en/test-doubles.html#test-doubles.mock-objects

    例如,如果您的类看起来像这样:

    <?php
    class AuditLogListener 
    {
        ...
        function postPersist($event)
        {
            $animal = new Animal();
            $em = $event->getEm();
            $em->persist($animal);
        }
        ...
    }
    

    您的测试应如下所示:

    <?php
    class AuditLogListenerTest
    {
        private $em;
        ...
        function testPostPersist()
        {
            $em = $this->getMockBuilder('stdClass')
                     ->setMethods(array('persist'))
                     ->getMock();
    
            $em->expects($this->once())
                     ->method('persist')
                     ->with($this->isInstanceOf('Animal'));
    
            $event = $this->getMockBuilder('stdClass')
                     ->setMethods(array('getEm'))
                     ->getMock();
    
            $event->expects($this->once())
                     ->method('getEm')
                     ->will($this->returnValue($em));
    
            $listener = new AuditLogListener();
            $listener->postPersist($event);
        }
        ...
    }
    

    有更多易于使用的模拟框架,如预言(https://github.com/phpspec/prophecy),但他们可能需要更多时间来处理它们 .

  • 4

    几乎不可能对共享包进行功能测试,因为您不能依赖Symfony2发行版 . 我认为在这种情况下,最好的办法是正确地对你的捆绑进行单元测试 . - olaurendeau

    以下是与侦听器相关的测试类(类的100%覆盖率):

    <?php
    
    namespace Kali\AuditLoggerBundle\Tests\Controller;
    
    use Kali\AuditLoggerBundle\Entity\AuditLog;
    use Kali\AuditLoggerBundle\EventListener\AuditLoggerListener;
    use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
    
    /**
     * Class AuditLoggerListenerTest
     * @package Kali\AuditLoggerBundle\Tests\Controller
     */
    class AuditLoggerListenerTest extends WebTestCase
    {
        protected static $container;
    
        /**
         * This method is called before the first test of this test class is run.
         *
         * @since Method available since Release 3.4.0
         */
        public static function setUpBeforeClass()
        {
            self::$container = static::createClient()->getContainer();
        }
    
    /*
     * ===========================================================================
     * TESTS ON AUDITABLE ENTITIES
     * ===========================================================================
     */
        /**
         * test prepersist function
         */
        public function testPrePersistWithAuditableEntity()
        {
            //Mock all the needed objects
            $token          =   $this->mockToken();
            $tokenStorage   =   $this->mockTokenStorage();
            $eventManager   =   $this->mockEventManager();
            $entityManager  =   $this->mockEntityManager();
            $entity         =   $this->mockEntity();
            $lifeCycleEvent =   $this->mockEvent('LifecycleEventArgs');
    
            //assert the methods that must be called or not
            $token          ->  expects($this->once())->method('getUsername');
            $tokenStorage   ->  expects($this->once())->method('getToken')->willReturn($token);
            $eventManager   ->  expects($this->once())->method('removeEventListener');
            $entityManager  ->  expects($this->once())->method('getEventManager')->willReturn($eventManager);
            $entityManager  ->  expects($this->once())->method('persist');
            $lifeCycleEvent ->  expects($this->never())->method('getEntityChangeSet');
            $lifeCycleEvent ->  expects($this->once())->method('getEntityManager')->willReturn($entityManager);
            $lifeCycleEvent ->  expects($this->once())->method('getEntity')->willReturn($entity);
    
            //instanciate the listener
            $listener = new AuditLoggerListener(
                new AuditLog(),
                self::$container->get('jms_serializer'),//Yes this is not really good to do that
                $tokenStorage
            );
            // call the function to test
            $listener->prePersist($lifeCycleEvent);
        }
    
        /**
         * test preUpdate function
         */
        public function testPreUpdateWithAuditableEntity()
        {
            //Mock all the needed objects
            $token          =   $this->mockToken();
            $tokenStorage   =   $this->mockTokenStorage();
            $eventManager   =   $this->mockEventManager();
            $entityManager  =   $this->mockEntityManager();
            $entity         =   $this->mockEntity();
            $lifeCycleEvent =   $this->mockEvent('PreUpdateEventArgs');
    
            //assert the methods that must be called or not
            $token          ->  expects($this->once())->method('getUsername');
            $tokenStorage   ->  expects($this->once())->method('getToken')->willReturn($token);
            $eventManager   ->  expects($this->once())->method('removeEventListener');
            $entityManager  ->  expects($this->once())->method('getEventManager')->willReturn($eventManager);
            $entityManager  ->  expects($this->once())->method('persist');
            $lifeCycleEvent ->  expects($this->once())->method('getEntityChangeSet');
            $lifeCycleEvent ->  expects($this->once())->method('getEntityManager')->willReturn($entityManager);
            $lifeCycleEvent ->  expects($this->once())->method('getEntity')->willReturn($entity);
    
            //instanciate the listener
            $listener = new AuditLoggerListener(
                new AuditLog(),
                self::$container->get('jms_serializer'),//Yes this is not really good to do that
                $tokenStorage
            );
            // call the function to test
            $listener->preUpdate($lifeCycleEvent);
        }
    
        /**
         * test PreRemove function
         */
        public function testPreRemoveWithAuditableEntity()
        {
            //Mock all the needed objects
            $token          =   $this->mockToken();
            $tokenStorage   =   $this->mockTokenStorage();
            $eventManager   =   $this->mockEventManager();
            $entityManager  =   $this->mockEntityManager();
            $entity         =   $this->mockEntity();
            $lifeCycleEvent =   $this->mockEvent('LifecycleEventArgs');
    
            //assert the methods that must be called or not
            $token          ->  expects($this->once())->method('getUsername');
            $tokenStorage   ->  expects($this->once())->method('getToken')->willReturn($token);
            $eventManager   ->  expects($this->once())->method('removeEventListener');
            $entityManager  ->  expects($this->once())->method('getEventManager')->willReturn($eventManager);
            $entityManager  ->  expects($this->once())->method('persist');
            $lifeCycleEvent ->  expects($this->never())->method('getEntityChangeSet');
            $lifeCycleEvent ->  expects($this->once())->method('getEntityManager')->willReturn($entityManager);
            $lifeCycleEvent ->  expects($this->once())->method('getEntity')->willReturn($entity);
    
            //instanciate the listener
            $listener = new AuditLoggerListener(
                new AuditLog(),
                self::$container->get('jms_serializer'),//Yes this is not really good to do that
                $tokenStorage
            );
            // call the function to test
            $listener->preRemove($lifeCycleEvent);
        }
    
    /*
     * ===========================================================================
     * TESTS ON NON AUDITABLE ENTITIES
     * ===========================================================================
     */
        /**
         * test prepersit function
         */
        public function testPrePersistWithNonAuditableEntity()
        {
            //Mock all the needed objects
            $token          =   $this->mockToken();
            $tokenStorage   =   $this->mockTokenStorage();
            $eventManager   =   $this->mockEventManager();
            $entityManager  =   $this->mockEntityManager();
            $entity         =   new AuditLog();//this entity is non Auditable
            $lifeCycleEvent =   $this->mockEvent('LifecycleEventArgs');
    
            //assert the methods that must be called or not
            $token          ->  expects($this->never())->method('getUsername');
            $tokenStorage   ->  expects($this->never())->method('getToken')->willReturn($token);
            $eventManager   ->  expects($this->once())->method("removeEventListener");
            $entityManager  ->  expects($this->never())->method('persist');
            $entityManager  ->  expects($this->once())->method('getEventManager')->willReturn($eventManager);
            $lifeCycleEvent ->  expects($this->never())->method('getEntityChangeSet');
            $lifeCycleEvent ->  expects($this->once())->method('getEntityManager')->willReturn($entityManager);
            $lifeCycleEvent ->  expects($this->once())->method('getEntity')->willReturn($entity);
    
            $listener = new AuditLoggerListener(
                new AuditLog(),
                self::$container->get('jms_serializer'),
                $tokenStorage
            );
    
            $listener->prePersist($lifeCycleEvent);
        }
    
        /**
         * test prepersit function
         */
        public function testPreUpdateWithNonAuditableEntity()
        {
            //Mock all the needed objects
            $token          =   $this->mockToken();
            $tokenStorage   =   $this->mockTokenStorage();
            $eventManager   =   $this->mockEventManager();
            $entityManager  =   $this->mockEntityManager();
            $entity         =   new AuditLog();//this entity is non Auditable
            $lifeCycleEvent =   $this->mockEvent('PreUpdateEventArgs');
    
            //assert the methods that must be called or not
            $token          ->  expects($this->never())->method('getUsername');
            $tokenStorage   ->  expects($this->never())->method('getToken')->willReturn($token);
            $eventManager   ->  expects($this->once())->method("removeEventListener");
            $entityManager  ->  expects($this->never())->method('persist');
            $entityManager  ->  expects($this->once())->method('getEventManager')->willReturn($eventManager);
            $lifeCycleEvent ->  expects($this->never())->method('getEntityChangeSet');
            $lifeCycleEvent ->  expects($this->once())->method('getEntityManager')->willReturn($entityManager);
            $lifeCycleEvent ->  expects($this->once())->method('getEntity')->willReturn($entity);
    
            $listener = new AuditLoggerListener(
                new AuditLog(),
                self::$container->get('jms_serializer'),
                $tokenStorage
            );
    
            $listener->preUpdate($lifeCycleEvent);
        }
    
        /**
         * test preRemove function
         */
        public function testPreRemoveWithNonAuditableEntity()
        {
            //Mock all the needed objects
            $token          =   $this->mockToken();
            $tokenStorage   =   $this->mockTokenStorage();
            $eventManager   =   $this->mockEventManager();
            $entityManager  =   $this->mockEntityManager();
            $entity         =   new AuditLog();//this entity is non Auditable
            $lifeCycleEvent =   $this->mockEvent('LifecycleEventArgs');
    
            //assert the methods that must be called or not
            $token          ->  expects($this->never())->method('getUsername');
            $tokenStorage   ->  expects($this->never())->method('getToken')->willReturn($token);
            $eventManager   ->  expects($this->once())->method("removeEventListener");
            $entityManager  ->  expects($this->never())->method('persist');
            $entityManager  ->  expects($this->once())->method('getEventManager')->willReturn($eventManager);
            $lifeCycleEvent ->  expects($this->never())->method('getEntityChangeSet');
            $lifeCycleEvent ->  expects($this->once())->method('getEntityManager')->willReturn($entityManager);
            $lifeCycleEvent ->  expects($this->once())->method('getEntity')->willReturn($entity);
    
            $listener = new AuditLoggerListener(
                new AuditLog(),
                self::$container->get('jms_serializer'),
                $tokenStorage
            );
    
            $listener->preRemove($lifeCycleEvent);
        }
    
    /*
     * ===========================================================================
     * MOCKS
     * ===========================================================================
     */
    
        /**
         * Mock a Token object
         *
         * @return \PHPUnit_Framework_MockObject_MockObject
         */
        private function mockToken()
        {
            $token = $this->getMock(
                'Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken',
                ['getUsername'],
                [],
                '',
                false
            );
    
            return $token;
        }
    
        /**
         * Mock a TokenStorage object
         *
         * @return \PHPUnit_Framework_MockObject_MockObject
         */
        private function mockTokenStorage()
        {
            //mock tokenStorage
            $tokenStorage = $this->getMock(
                'Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage',
                ['getToken'],
                [],
                '',
                false
            );
    
            return $tokenStorage;
        }
    
        /**
         * Mock an EventManager Object
         *
         * @return \PHPUnit_Framework_MockObject_MockObject
         */
        private function mockEventManager()
        {
            //mock the event manager
            $eventManager = $this->getMock(
                '\Doctrine\Common\EventManager',
                ['removeEventListener'],
                [],
                '',
                false
            );
    
            return $eventManager;
        }
    
        /**
         * Mock an EntityManager
         *
         * @return \PHPUnit_Framework_MockObject_MockObject
         */
        private function mockEntityManager()
        {
            //mock the entityManager
            $emMock = $this->getMock(
                '\Doctrine\ORM\EntityManager',
                ['getEventManager', 'persist', 'update', 'remove', 'flush'],
                [],
                '',
                false
            );
    
            return $emMock;
        }
    
        /**
         * Mock an Entity Object
         *
         * @return \PHPUnit_Framework_MockObject_MockObject
         */
        private function mockEntity()
        {
            $entity = $this->getMockBuilder('stdClass')
                           ->setMethods(['getName', 'getType'])
                           ->getMock();
    
            $entity->expects($this->any())
                   ->method('getName')
                   ->will($this->returnValue('toto'));
            $entity->expects($this->any())
                   ->method('getType')
                   ->will($this->returnValue('chien'));
    
            return $entity;
        }
    
        /**
         * mock a lifeCycleEventArgs Object
         *
         * @param $eventType
         *
         * @return \PHPUnit_Framework_MockObject_MockObject
         */
        private function mockEvent($eventType)
        {
            $lifeCycleEvent = $this->getMock(
                '\Doctrine\ORM\Event\\'.$eventType,
                ['getEntityManager', 'getEntity', 'getEntityChangeSet'],
                [],
                '',
                false
            );
    
            return $lifeCycleEvent;
        }
    }
    

    如果您对此有任何意见,请发表评论:)(例如我可以将"mock all needed objects"部分重构为一个函数)

相关问题