Using Doctrine Data Fixture for Testing QueryBuilder inside Repository
This is an immediate post to incorporate my latest post about Mocking Doctrine\ORM\AbstractQuery to test querybuilder. Well, while that works, Ocramius – A Doctrine core team – said that we shouldn’t do that way, as SQL and DQL are actual code. What we can do is using data fixture. There is a sample that he gave with ZF2 environment. In this post, I will try to write a demo with step by step about repository class that consume a QueryBuilder, so we can test it.
Let’s start with the Entity
Like my post before, I will going to use News
entity :
namespace DoctrineFixtureDemo\Entity; use Doctrine\ORM\Mapping as ORM; /** * News * * @ORM\Table(name="news") * @ORM\Entity(repositoryClass="DoctrineFixtureDemo\Repository\NewsRepository") */ class News { /** * @var integer * * @ORM\Column(name="id", type="bigint", nullable=false) * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ private $id; /** * @var string * * @ORM\Column(name="title", type="string", length=50, nullable=false) */ private $title; /** * @var string * * @ORM\Column(name="content", type="string", length=255, nullable=false) */ private $content; /** * Get id. * * @return integer */ public function getId() { return $this->id; } /** * Set title. * * @param string $title * * @return self */ public function setTitle($title) { $this->title = $title; return $this; } /** * Get title. * * @return string */ public function getTitle() { return $this->title; } /** * Set content. * * @param string $content * * @return self */ public function setContent($content) { $this->content = $content; return $this; } /** * Get content. * * @return string */ public function getContent() { return $this->content; } }
The Repository
I have a repository to get latest news with limit parameter, like the following :
namespace DoctrineFixtureDemo\Repository; use Doctrine\ORM\EntityRepository; class NewsRepository extends EntityRepository { /** * Get Latest News. * * @param int $limit * * @return array */ public function getLatestNews($limit) { $result = $this->createQueryBuilder('n') ->setFirstResult(0) ->setMaxResults($limit) ->getQuery()->getResult(); return $result; } }
Fixture Dependency
We need "doctrine/data-fixtures"
dependency in our vendor/, we can require by composer command :
$ composer require "doctrine/data-fixtures 1.0.*"
Fixture Class
We can create a fixture class to insert some sample data to be tested.
namespace DoctrineFixtureDemo\DataFixture; use DoctrineFixtureDemo\Entity\News; use Doctrine\Common\Persistence\ObjectManager; use Doctrine\Common\DataFixtures\FixtureInterface; class NewsLoad implements FixtureInterface { public function load(ObjectManager $manager) { $news = new News(); $news->setTitle('bar'); $news->setContent('BarBazBat'); $news2 = new News(); $news2->setTitle('bar2'); $news2->setContent('BarBazBat2'); $manager->persist($news); $manager->persist($news2); $manager->flush(); } }
We are going to use Doctrine\Common\DataFixtures\Executor\ORMExecutor
to execute loaded fixture class(es). We can setup fixture test by use database that only for test / not for production ( – note : this is just sample, you can rely on your framework for get the db setting, EntityManager, SchemaTool, etc – ) by creating class like the following :
namespace DoctrineFixtureDemotest; use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\AnnotationRegistry; use Doctrine\Common\DataFixtures\Purger\ORMPurger; use Doctrine\Common\DataFixtures\Executor\ORMExecutor; use Doctrine\ORM\Tools\Setup; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\Driver\AnnotationDriver; use Doctrine\ORM\Tools\SchemaTool; final class FixtureManager { /** * Get EntityManager */ public static function getEntityManager() { $paths = [dirname(__DIR__).'/src/DoctrineFixtureDemo/Entity']; $isDevMode = true; // the TEST DB connection configuration $connectionParams = [ 'driver' => 'pdo_mysql', 'user' => 'root', 'password' => '', 'dbname' => 'foobartest', ]; $config = Setup::createConfiguration($isDevMode); $driver = new AnnotationDriver(new AnnotationReader(), $paths); AnnotationRegistry::registerLoader('class_exists'); $config->setMetadataDriverImpl($driver); $entityManager = EntityManager::create($connectionParams, $config); return $entityManager; } /** * Make sure drop and create tables */ public static function start() { $schemaTool = new SchemaTool(static::getEntityManager()); $metadatas = static::getEntityManager() ->getMetadataFactory() ->getAllMetadata(); $schemaTool->dropSchema($metadatas); $schemaTool->createSchema($metadatas); } /** * @return ORMExecutor */ public static function getFixtureExecutor() { return new ORMExecutor( static::getEntityManager(), new ORMPurger(static::getEntityManager()) ); } }
In favor of above class, we can always call \DoctrineFixtureDemotest\FixtureManager::start();
at our phpunit’s bootstrap :
chdir(__DIR__); $loader = null; if (file_exists('../vendor/autoload.php')) { $loader = include '../vendor/autoload.php'; } else { throw new RuntimeException('vendor/autoload.php could not be found. Did you run `php composer.phar install`?'); } $loader->add('DoctrineFixtureDemotest', __DIR__); \DoctrineFixtureDemotest\FixtureManager::start();
So, all tables will be removed in the every beginning test.
Repository Test
Time for test, Our repository class test can be like the following :
namespace DoctrineFixtureDemoTest\Repository; use DoctrineFixtureDemo\DataFixture\NewsLoad; use DoctrineFixtureDemotest\FixtureManager; use DoctrineFixtureDemo\Repository\NewsRepository; use Doctrine\ORM\Mapping\ClassMetadata; use PHPUnit_Framework_TestCase; class NewsRepositoryTest extends PHPUnit_Framework_TestCase { protected function setUp() { $this->repository = new NewsRepository( FixtureManager::getEntityManager(), new ClassMetadata('DoctrineFixtureDemo\Entity\News') ); $this->fixtureExecutor = FixtureManager::getFixtureExecutor(); } public function testGetLatestNews() { $this->fixtureExecutor->execute([new NewsLoad()]); $this->assertCount(1, $this->repository->getLatestNews(1)); $this->assertInstanceOf('DoctrineFixtureDemo\Entity\News', $this->repository->getLatestNews(1)[0]); } }
If everything goes well, then your repository class will be tested.
Want to grab the sourcecode?, you can take a look https://github.com/samsonasik/DoctrineFixtureDemo
Another approach
You can already have prepared tables before test executed, and call truncate in every tearDown()
at your unit test class as Manuel Stosic suggestion.
References :
1. https://twitter.com/Ocramius/status/577979551281729536
2. https://github.com/doctrine/data-fixtures
3. https://gist.github.com/Ocramius/3994325
[…] Ocramius suggest to not do it. He suggest to use Fixture instead like his gist. I created a new post in favor of […]
$result = $this->_em->getRepository($this->getEntityName())
equals
$result = $this
You’re already inside the repository, so no need to get the repository ^^ Other than that, good quick overview on how to set up Fixtures quickly.
One hint though: I’d always run a TRUNCATE TABLE on the UnitTests tearDown() so I can be absolutely certain that other tests won’t have problems. Every test case should load it’s own fixture ultimately.
thank you ;). it’s updated
Good approach!
By the way, doing this inside a repository class $this->_em->getRepository($this->getEntityName()) won’t be the same as $this? 😉
Thank you, Yes, it’s updated 😉
Just want to say thank you. This is still some dark magic to me, but I will keep staring and blinking, poking and prodding, following along in a monkey-see-monkey-do fashion until I begin to really understand all the wiring.
Right now I’ve been attempting to combine the simple bootstrap.php provided in your repo with the more elaborate Bootstrap.php suggested at http://framework.zend.com/manual/current/en/tutorials/unittesting.html, so we can run one phpunit command using a single bootstrap for all the tests in a ZF module, i.e., testing both controllers and entities. I seem to have a crude solution but maybe will return with another comment if I come up with something useful.
Thank you! This was exactly what I was looking for!
I am following this recipe, but extending Zend\Test\PHPUnit\Controller\AbstractHttpControllerTestCase. In my setup(), I am doing like so:
$fixtureExecutor = FixtureManager::getFixtureExecutor();
$fixtureExecutor->execute([
new DataFixture\JudgeLoader,
new DataFixture\UserLoader,
new DataFixture\LanguageLoader,
new DataFixture\EventTypeLoader,
new DataFixture\DefendantNameLoader,
new DataFixture\RequestLoader,
]);
$this->setApplicationConfig(
include ‘config/application.config.php’
);
/* etc */
These entities have various relationships to one another, e.g., many-to-one, and I need them all in order to to test certain controller actions.
In my service manager configuration I am setting up Doctrine’s FileSystemCache to reduce the database queries. In my “development” environment I am using MySQL and it’s working great, as best I can tell. But in my “testing” environment I am using SQLite, because it’s been recommended and I thought its speed would be an advantage. And there, I am getting “PDOException: SQLSTATE[HY000]: General error: 5 database is locked” timeout errors whenever the cache is enabled. If I disable it, all goes smoothly.
Any idea what I might be doing wrong here?
I’ve no idea, sorry. My suggestion is, if your app is using MySQL, just use MySQL as well for testing so you get “test real app db”.
thanks for replying. you know what? I wanted to withdraw this comment before it was too late 🙂 . It was one of those things where the symptoms of the problem seemed so remote from the cause that it’s no wonder I had a hard time figuring it out. it turns out it was related to the way I was initializing sqlite and adding a user-defined function. it appeared to be working, but not because I was doing it right but because I was lucky. when I added caching to the mix, my luck ran out and my unit tests crashed and burned. i figured out the right way to do it by reading the source code. but i won’t go into detail — the whole issue is off-topic with respect to your excellent blog post here, so I won’t be offended if you remove this little comment thread. thanks again!