php - symfonytestslistener - symfony test querybuilder
¿Cómo lograr el aislamiento de prueba con los transformadores de datos y formularios de Symfony? (1)
En primer lugar, no tengo experiencia con Symfony. Sin embargo, creo que te perdiste una tercera opción allí. Al trabajar de manera efectiva con el código heredado, Michael Feathers describe una forma de aislar las dependencias mediante el uso de la herencia (lo llama "Extraer e invalidar").
Dice así:
class HiddenEntityType extends AbstractType
{
/* stuff */
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options[''multiple'']) {
$builder->addViewTransformer(
$this->createEntitiesToPrimaryKeysTransformer($options)
);
}
}
protected function createEntitiesToPrimaryKeysTransformer(array $options)
{
return new EntitiesToPrimaryKeysTransformer(
$this->em->getRepository($options[''class'']),
$options[''get_pk_callback''],
$options[''identifier'']
);
}
}
Ahora para probar, creas una nueva clase, FakeHiddenEntityType
, que extiende HiddenEntityType
.
class FakeHiddenEntityType extends HiddenEntityType {
protected function createEntitiesToPrimaryKeysTransformer(array $options) {
return $this->mock;
}
}
Donde $this->mock
obviamente es lo que necesites que sea.
Las dos ventajas más importantes son que no hay fábricas involucradas, por lo que la complejidad aún está encapsulada, y prácticamente no hay posibilidad de que este cambio rompa el código existente.
La desventaja es que esta técnica requiere una clase extra. Más importante aún, requiere una clase que sepa sobre los aspectos internos de la clase bajo prueba.
Para evitar la clase extra, o más bien ocultar la clase extra, uno podría encapsularlo en una función, creando una clase anónima en su lugar (el soporte para clases anónimas se agregó en PHP 7).
class HiddenEntityTypeTest extends TestCase
{
private function createHiddenEntityType()
{
$mock = ...; // Or pass as an argument
return new class extends HiddenEntityType {
protected function createEntitiesToPrimaryKeysTransformer(array $options)
{
return $mock;
}
}
}
public function testABC()
{
$type = $this->createHiddenEntityType();
/* ... */
}
}
Nota: esto es Symfony <2.6, pero creo que el mismo problema general se aplica independientemente de la versión
Para comenzar, considere este tipo de formulario que está diseñado para representar una o más entidades como un campo oculto (las cosas del espacio de nombres se omiten por brevedad)
class HiddenEntityType extends AbstractType
{
/**
* @var EntityManager
*/
protected $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options[''multiple'']) {
$builder->addViewTransformer(
new EntitiesToPrimaryKeysTransformer(
$this->em->getRepository($options[''class'']),
$options[''get_pk_callback''],
$options[''identifier'']
)
);
} else {
$builder->addViewTransformer(
new EntityToPrimaryKeyTransformer(
$this->em->getRepository($options[''class'']),
$options[''get_pk_callback'']
)
);
}
}
/**
* See class docblock for description of options
*
* {@inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
''get_pk_callback'' => function($entity) {
return $entity->getId();
},
''multiple'' => false,
''identifier'' => ''id'',
''data_class'' => null,
));
$resolver->setRequired(array(''class''));
}
public function getName()
{
return ''hidden_entity'';
}
/**
* {@inheritdoc}
*/
public function getParent()
{
return ''hidden'';
}
}
Esto funciona, es sencillo, y en su mayor parte se parece a todos los ejemplos que ve para agregar transformadores de datos a un tipo de formulario. Hasta que llegues a las pruebas unitarias. ¿Ves el problema? Los transformadores no pueden ser burlados. "¡Pero espera!" usted dice: "Las pruebas unitarias para los formularios de Symfony son pruebas de integración, se supone que deben asegurarse de que los transformadores no fallen. ¡Incluso lo dice la documentación !"
Esta prueba verifica que ninguno de sus transformadores de datos utilizados por el formulario haya fallado. El método isSynchronized () solo se establece en falso si un transformador de datos lanza una excepción
Ok, entonces vives con el hecho de que no puedes aislar los transformadores. ¿No es gran cosa?
Ahora considere qué sucede cuando la unidad prueba un formulario que tiene un campo de este tipo (suponga que HiddenEntityType
se ha definido y etiquetado en el contenedor de servicios)
class SomeOtherFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(''field'', ''hidden_entity'', array(
''class'' => ''AppBundle:EntityName'',
''multiple'' => true,
));
}
/* ... */
}
Ahora entra el problema. La prueba de unidad para SomeOtherFormType
ahora necesita implementar getExtensions()
para que hidden_entity
tipo hidden_entity
. Entonces, ¿cómo se ve?
protected function getExtensions()
{
$mockEntityManager = $this
->getMockBuilder(''Doctrine/ORM/EntityManager'')
->disableOriginalConstructor()
->getMock();
/* Expectations go here */
return array(
new PreloadedExtension(
array(''hidden_entity'' => new HiddenEntityType($mockEntityManager)),
array()
)
);
}
¿Ves donde está ese comentario en el medio? Sí, para que esto funcione correctamente, todas las simulaciones y expectativas que se encuentran en la clase de prueba unitaria para el HiddenEntityType
ahora deben duplicarse aquí. No estoy de acuerdo con esto, ¿cuáles son mis opciones?
Inyectar el transformador como una de las opciones.
Esto sería muy sencillo y simplificaría la burla, pero en última instancia simplemente patea la lata por el camino. Porque en este escenario, el
new EntityToPrimaryKeyTransformer()
simplemente se movería de una clase de tipo de formulario a otra. Sin mencionar que creo que los tipos de formas deberían ocultar su complejidad interna del resto del sistema. Esta opción significa empujar esa complejidad fuera del límite del tipo de formulario.Inyectar una fábrica de transformadores de tipo en el tipo de formulario.
Este es un enfoque más típico para eliminar "newables" de un método, pero no puedo evitar la sensación de que esto se está haciendo solo para hacer que el código sea verificable, y en realidad no lo está mejorando. Pero si se hiciera, se vería algo así.
class HiddenEntityType extends AbstractType { /** * @var DataTransformerFactory */ protected $transformerFactory; public function __construct(DataTransformerFactory $transformerFactory) { $this->transformerFactory = $transformerFactory; } public function buildForm(FormBuilderInterface $builder, array $options) { $builder->addViewTransformer( $this->transformerFactory->createTransfomerForType($this, $options); ); } /* Rest of type unchanged */ }
Esto se siente bien hasta que considero cómo será realmente la fábrica. Necesitará el administrador de la entidad inyectado, para empezar. Pero ¿entonces qué? Si miro más adelante, esta fábrica supuestamente genérica podría necesitar todo tipo de dependencias para crear transformadores de datos de diferentes tipos. Eso claramente no es una buena decisión de diseño a largo plazo. Entonces, ¿qué? ¿Volver a etiquetar esto como un
EntityManagerAwareDataTransformerFactory
? Está empezando a sentirse desordenado aquí.Cosas que no estoy pensando ...
¿Pensamientos? ¿Experiencias? ¿Un consejo sólido?