TLDR: I am new to unit tests and I have few questions:
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);
}