Symfony2 Doctrine MongoDB rollback

doctrine-orm, mongodb, php, symfony, tdd

Solution

I am not using two separate DBs for development and testing

That's the first thing to address - because without a testing db, running tests will affect your development db and vice versa which is a terrible idea. You should be able to run tests in your production environment with absolute confidence that nothing you do in a test will affect your deployed site.

Setup a test connection

So, modify your parameters.yml to have something like this:

database.host: localhost
database.port: 27017
database.db:   myappname

database.test.host: localhost
database.test.port: 27017
database.test.db:   myappname-test

In addition, in your app/config/config_test.yml file override the default connnection so that anything you trigger as part of a test which requests the default document manager will receive a manager pointing at your test db:

doctrine_mongodb:
    document_managers:
        default:
            database: %database.test.db%

Prepare for tests with fixtures

Then, what you want to do effectively is:

- truncate relevant collections

- load fixtures

on your test db before each test.

Here's an example abstract test class:

<?php

use Doctrine\Common\DataFixtures\Executor\MongoDBExecutor as Executor,
    Doctrine\Common\DataFixtures\Purger\MongoDBPurger as Purger,
    Doctrine\Common\DataFixtures\Loader,
    Doctrine\Common\DataFixtures\ReferenceRepository,
    Symfony\Bundle\FrameworkBundle\Test\WebTestCase,
    Symfony\Bundle\FrameworkBundle\Console\Application;

abstract class AbstractTest extends WebTestCase
{
    /**
     * Array of fixtures to load.
     */
    protected $fixtures = array();

    /**
     * Setup test environment
     */
    public function setUp()
    {
        $kernel = static::createKernel(array('environment' => 'test', 'debug' => false));
        $kernel->boot();
        $this->container = $kernel->getContainer();
        $this->dm = $this->container->get('doctrine.odm.mongodb.document_manager');

        if ($this->fixtures) {
            $this->loadFixtures($this->fixtures, false);
        }
    }

    /**
     * Load fixtures
     *
     * @param array   $fixtures names of _fixtures to load
     * @param boolean $append   append data, or replace?
     */
    protected function loadFixtures($fixtures = array(), $append = true)
    {
        $defaultFixtures = false;

        $loader = new Loader();
        $refRepo = new ReferenceRepository($this->dm);

        foreach ((array) $fixtures as $name) {
            $fixture = new $name();
            $fixture->setReferenceRepository($refRepo);
            $loader->addFixture($fixture);
        }

        $purger = new Purger();
        $executor = new Executor($this->dm, $purger);
        $executor->execute($loader->getFixtures(), $append);
    }
}

Use fixtures in your tests

With the previous abstract test class, you can then write tests which use your fixture data - or not - as appropriate. Below is a trivial example.

<?php

use Your\AbstractTest,
    Your\Document\Foo;

class RandomTest extends AbstractTest
{
    /**
     * fixtures to load before each test
     */
    protected $fixtures = array(
        'APP\FooBundle\DataFixtures\MongoDB\TestFoos',
        'APP\FooBundle\DataFixtures\MongoDB\TestBars'
    );

    ...

    /**
     * Check it gets an ID (insert succeeded)
     * 
     */
    public function testCreateDefaults()
    {
        $foo = new Foo();
        $this->dm->persist($foo);
        $this->dm->flush();

        $this->assertNotNull($foo->getId());
        $this->assertSame('default value', $foo->getSomeProperty());
        // etc.
    }

    /**
     * Check result of something with a given input
     * 
     */
    public function testSomething()
    {
        $foo = $this->dm->getRepository(APPFooBundle:Foo)->findByName('Some fixture object');

        $foo->doSomething();
        $this->assertSame('modified value', $foo->getSomeProperty());
        // etc.
    }

Before each test, the fixtures you've defined will be loaded (truncating the collections they affect), giving a consistent db state on which to base your tests.

Problem

I am trying to build a secure of set of tests with Symfony2, Doctrine and MongoDB. What I need to do is to load a lot of fixtures when a test begin, and unload them once it ends. I thought of doing it with a transaction, but... I couldn't find documentation on how to do it with Doctrine and Mongo! I found good documentation in the Doctrine docs regarding how to do transactions with the ORM, but not regarding the ODM. So I took a look at the source code of the `Connection.php` class used by Doctrine-Mongo too and I haven't found the `beginTransaction`, `commit`and `rollback` methods that the dbal version uses. I was clueless, then I asked myself "Is it even possible to rollback in MongoDB?", and the answer if found in the MongoDB FAQ was: MongoDB does not use traditional locking or complex transactions with rollback :( So I guess that's why there is no `beginTransaction` or whatsoever in the ODM... But my problem remains: how can I implement a sort of rollback for my tests? The only idea I got right now is to manually get all the ids of the Document I load and then remove them in the `tearDown()`. But, well... it kinda sucks, doesn't it? Other ideas?? EDIT: After my first comment to this question, regarding the fact that I want to have the same DB in test and development, I thought: why don't use a separate test database, where the development database gets copied when the tests start, and that can be light-heartedly dropped? Could it be a better idea? It actually looks easier and more secure to me. What do you guys think? Thanks :)

Original source