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


How to test doctrine EventListener/Subscriber without any entity

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

这个包与我的其他包一起工作得很好,但是我想对它进行单元测试和功能测试。

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

在这个包中,我不知道如何做到这一点,因为我只有一个AuditLog实体,我需要使用两个over实体(将只在测试中使用)。

  1. 第一个实体将是"可审计的"(我必须有一个新的条目在
  2. 如果我在这个实体上做一个持久化,更新或删除,audit_log)。
  3. 第二个将是"不可审计的"(我不能有一个新的条目在audit_log表中执行持久化、更新或删除操作这个实体)。*
  4. 两个实体可以关联到一个唯一的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

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

EDIT_1:阅读您的答案后,将执行AuditLoggerListener函数的单元测试,但我仍然想进行功能测试

*是的,我知道有很多,但他们不符合我所寻找的。

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

EDIT_2:这里是代码服务:

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函数…因此,例如,我需要测试当我对假实体(我真的不知道如何模拟)进行持久化时,是否在我的audit_log表中有一个新条目…

在共享包上做功能测试几乎是不可能的,因为你不能依赖Symfony2发行版。我认为在这种情况下,最好的做法是对bundle进行适当的单元测试。——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"部分重构为一个函数)

单元测试一个php类意味着只测试这个类包含的代码,不需要任何外部交互。因此,您应该模拟所有外部服务:参见phpunit模拟文档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),但它们可能需要更多的时间来处理它们。