How to unit test a Symfony2 form when it uses a transformer linked to a database
TLDR: I am new to unit tests and I have few questions:
- Are my transformer tests well written?
- Is there a way to decoupled my transformer tests from the database?
- How to test my form with the transformer using the database?
- Should I decouple my form from my transformer?
I don't know if my classes are too coupled, if my design is flawed or if my understanding of the unit tests is bad.
Here is some background. I have a form object with different widgets. One of them is used within a model transformer. This model transformer uses a connection to the database to retrieve the proper object.
Here is my code:
class BookToStringTransformer implements DataTransformerInterface { private $om; public function __construct(ObjectManager $om) { $this->om = $om; } public function transform($book) { if (!$book instanceof Book) { return ""; } return $book->getName(); } public function reverseTransform($string) { if (!is_string($string) || !$string) { return null; } $book = $this->om ->getRepository('MyBundle:Book') ->findOneBy(array('name' => $string)) ; if (null === $book) { throw new TransformationFailedException(sprintf( 'The book "%s" does not exist!', $string )); } return $book; } } class ItemType extends AbstractType { private $om; public function __construct(ObjectManager $om) { $this->om = $om; } public function buildForm(FormBuilderInterface $builder, array $options) { $bookTransformer = new BookToStringTransformer($this->om); $builder->add($builder->create('book', 'text', array( 'required' => false, ))->addModelTransformer($bookTransformer)); } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'MyBundle\Entity\Item', )); } public function getName() { return 'mybundle_item'; } }
I wrote unit tests for the transformer using the KernelTestCase
class BookToStringTransformerTest extends KernelTestCase { private $name = 'existing name'; private $em; public function setUp() { static::$kernel = static::createKernel(); static::$kernel->boot(); $this->em = static::$kernel->getContainer() ->get('doctrine') ->getManager(); } public function testReverseTransform_whenNameExists_returnsBookObject() { $transformer = new BookToStringTransformer($this->em); $book = $transformer->reverseTransform($this->name); $this->assertInstanceOf('MyBundle\Entity\Book', $book, 'Should return a Book object'); $this->assertEquals($this->name, $book->getName(), 'Should return a Book object with the selected name'); } /** * @expectedException Symfony\Component\Form\Exception\TransformationFailedException */ public function testReverseTransform_whenNameDoesNotExist_throwsException() { $transformer = new BookToStringTransformer($this->em); $transformer->reverseTransform('unknown name'); } /** * @param mixed $invalid_parameter * @dataProvider provideInvalidParameter */ public function testReverseTransform_whenParameterIsInvalid_returnsNull($invalid_parameter) { $om = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager')->getMock(); $transformer = new BookToStringTransformer($om); $this->assertNull($transformer->reverseTransform($invalid_parameter), 'Should return a NULL value'); } /** * @return array */ public function provideInvalidParameter() { return [ [null], [false], [true], [''], [[]], [new \stdClass()], ]; } public function testTransform_whenParameterIsBookObject_returnsName() { $book = $this->em->getRepository('MyBundle:Book') ->findOneBy(array('name' => $this->name)); $om = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager')->getMock(); $transformer = new BookToStringTransformer($om); $this->assertEquals($this->name, $transformer->transform($book), 'Should return a string containing the name'); } /** * @param mixed $not_book * @dataProvider provideInvalidBookObject */ public function testTransform_whenParameterIsNotBookObject_returnsEmptyString($not_book) { $om = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager')->getMock(); $transformer = new BookToStringTransformer($om); $this->assertEquals("", $transformer->transform($not_book), 'Should return an empty string to be chained'); } /** * @return array */ public function provideInvalidBookObject() { return [ [null], [123], ['123'], [[]], [true], [new \stdClass()], ]; } }
As I am new to unit tests, I don't even know if it is the proper way to test that transformer. I start writing tests for the form object. I am using the TypeTestCase but there is no simple way to get the connection to the database and I can't use the KernelTestCase.
class ItemTypeTest extends TypeTestCase { /** * @expectedException \PHPUnit_Framework_Error */ public function test_whenCreatedWithNoParameters_raiseException() { new ItemType(); } /** * @expectedException \PHPUnit_Framework_Error */ public function test_whenCreatedWithBadParameters_raiseException() { new ItemType(123); } public function test_whenCreatedWithGoodParameters_createsFormObject() { $om = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager')->getMock(); $type = new ItemType($om); $form = $this->factory->create($type); $this->assertInstanceOf('Symfony\Component\Form\Form', $form); } public function test_whenSubmittedWithGoodData() { $formData = array( 'name' => 'existing name', ); $om = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager')->getMock(); $type = new ItemType($om); $form = $this->factory->create($type); $form->submit($formData); } }
The last test fails because the transformer does get access to the database since I am passing a mock to the form. So should I get a real object (meaning classes are too coupled) or should I find an other way.
Thank you
The approach is good, in the last method you must mock the repo object and the repo response. In example try this code:
public function test_whenSubmittedWithGoodData() { $formData = array( 'name' => 'existing name', ); $om = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager')->getMock(); $repoMock= $this->getMock('Doctrine\ORM\EntityRepository', array(), array(), '', false); $om ->expects($this->atLeastOnce()) ->method('getRepository') ->withAnyParameters() ->will($this->returnValue($repoMock)); $repoMock ->expects($this->atLeastOnce()) ->method('findOneBy') ->withAnyParameters() ->will($this->returnValue($mockedBook)); $type = new ItemType($om); $form = $this->factory->create($type); $form->submit($formData); }