Как проверить доктрину EventListener / Subscriber без какой-либо сущности

Я создал AuditLoggerBundle *, в котором есть служба, использующая события Doctrine (prePersist, preUpdate и preRemove), чтобы создать новую запись в таблице AuditLog (AuditLog Entity).

Комплект отлично работает с другими моими комплектами, но я бы хотел модульное тестирование и функциональное тестирование.

Проблема в том, что для проведения функциональных испытаний на AuditLoggerListener функции, мне нужно иметь как минимум две «поддельные» сущности, которые я могу сохранить, обновить и т. д.

В этом комплекте я не знаю, как это сделать, потому что у меня просто есть сущность AuditLog, и мне нужно использовать две над сущностями (это будет использоваться только в тестах).

  1. Первая сущность будет «проверяемой» (у меня должна быть новая запись в
    audit_log, если я сохраню, обновлю или удалю эту сущность).
  2. Второй будет «не подлежит аудиту» (у меня не должно быть новой записи
    в таблице audit_log, когда я выполняю постоянное обновление, обновление или удаление
    это лицо). *
  3. Эти две сущности могут быть связаны с уникальным 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: после прочтения ваших ответов будут выполнены модульные тесты для функций 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 …

3

Решение

Модульное тестирование класса php означает просто тестирование кода, содержащегося в этом классе, без какого-либо внешнего взаимодействия.
Поэтому вы должны смоделировать все внешние сервисы: см. Документацию phpunit mock 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;
}
}

Если вам есть что сказать по этому поводу, пожалуйста, оставьте комментарий 🙂
(например, я могу перестроить часть «макетировать все необходимые объекты» в функцию)

2

По вопросам рекламы [email protected]