web-dev-qa-db-fra.com

Comment configurer des tests unitaires de bases de données lourdes dans Symfony2 avec PHPUnit?

Je suis assez nouveau dans le monde des tests et je veux m'assurer que je suis sur la bonne voie.

J'essaie de configurer des tests unitaires dans un projet symfony2 à l'aide de phpunit

PHPUnit fonctionne et les tests du contrôleur par défaut simples fonctionnent correctement. (Pourtant, il ne s'agit pas de tests fonctionnels mais de tests unitaires de mon application.)

Mon projet repose cependant beaucoup sur les interactions avec les bases de données, et d'après ce que je comprends de la documentation de phpunit , je devrais configurer une classe basée sur \PHPUnit_Extensions_Database_TestCase, puis créer des fixtures pour ma base de données et travailler à partir de là.

Cependant, symfony2 n'offre qu'une classe WebTestCase qui s'étend uniquement de \PHPUnit_Framework_TestCase prêt à l'emploi.

Alors, ai-je raison de supposer que je devrais créer ma propre DataBaseTestCase qui copie principalement WebTestCase, la seule différence étant qu'elle s'étend de \PHPUnit_Extensions_Database_TestCase et implémente toutes ses méthodes abstraites?

Ou existe-t-il un autre flux de travail "intégré" recommandé pour symfony2 concernant les tests centrés sur la base de données?

Comme je veux m'assurer que mes modèles stockent et récupèrent les bonnes données, je ne veux pas finir par tester les spécificités de doctrine par accident.

43
k0pernikus

tl; dr:

  • Si et seulement si vous voulez suivre toute la route du test fonctionnel, je vous recommande de regarder la réponse de Sgoettschkes .
  • Si vous souhaitez effectuer des tests unitaires de votre application et devez tester le code qui interagit avec la base de données, lisez-le ou passez directement à symfony2 docs


Certains aspects de ma question initiale montraient clairement que je ne comprenais pas bien les différences entre les tests unitaires et les tests fonctionnels. (Comme je l'ai écrit, je souhaite effectuer un test unitaire de l'application, mais parlait également du test du contrôleur en même temps; il s'agit de test fonctionnel par définition).

Le test unitaire n'a de sens que pour les services et non pour les référentiels. Et ces services peuvent utiliser des simulacres du responsable de l'entité. (J'irais même jusqu'à dire: si possible, écrivez des services qui n'attendent que des entités. Vous devez ensuite créer des simulacres de ces entités et vos tests unitaires de votre logique métier deviennent très simples.)

Mon cas d'utilisation réel pour mon application était assez bien réfléchi sur la documentation symfony2 sur comment tester le code qui interagit avec la base de données .

Ils fournissent cet exemple pour un test de service:

Classe de service:

use Doctrine\Common\Persistence\ObjectManager;

class SalaryCalculator
{
    private $entityManager;

    public function __construct(ObjectManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function calculateTotalSalary($id)
    {
        $employeeRepository = $this->entityManager
            ->getRepository('AppBundle:Employee');
        $employee = $employeeRepository->find($id);

        return $employee->getSalary() + $employee->getBonus();
    }
}

Classe de test de service:

namespace Tests\AppBundle\Salary;

use AppBundle\Salary\SalaryCalculator;
use AppBundle\Entity\Employee;
use Doctrine\ORM\EntityRepository;
use Doctrine\Common\Persistence\ObjectManager;

class SalaryCalculatorTest extends \PHPUnit_Framework_TestCase
{
    public function testCalculateTotalSalary()
    {
        // First, mock the object to be used in the test
        $employee = $this->getMock(Employee::class);
        $employee->expects($this->once())
            ->method('getSalary')
            ->will($this->returnValue(1000));
        $employee->expects($this->once())
            ->method('getBonus')
            ->will($this->returnValue(1100));

        // Now, mock the repository so it returns the mock of the employee
        $employeeRepository = $this
            ->getMockBuilder(EntityRepository::class)
            ->disableOriginalConstructor()
            ->getMock();
        $employeeRepository->expects($this->once())
            ->method('find')
            ->will($this->returnValue($employee));

        // Last, mock the EntityManager to return the mock of the repository
        $entityManager = $this
            ->getMockBuilder(ObjectManager::class)
            ->disableOriginalConstructor()
            ->getMock();
        $entityManager->expects($this->once())
            ->method('getRepository')
            ->will($this->returnValue($employeeRepository));

        $salaryCalculator = new SalaryCalculator($entityManager);
        $this->assertEquals(2100, $salaryCalculator->calculateTotalSalary(1));
    }
}

Aucune base de données de test requise pour ce type de test, seulement des moqueries.

Comme il est important de tester la logique métier, pas la couche de persistance.

Ce n'est que pour les tests fonctionnels qu'il est logique de disposer de sa propre base de données de tests, qu'il convient de créer et de démolir par la suite. La grande question devrait donc être: 

Quand le test fonctionnel a-t-il un sens?

J'avais l'habitude de penser que tester toutes les choses est la bonne réponse; pourtant, après avoir travaillé avec de nombreux logiciels existants qui, à eux seuls, étaient à peine développés, je suis devenu un peu plus paresseuxpragmatique et considérez certaines fonctionnalités comme fonctionnant jusqu’à preuve du contraire d’un bogue.

Supposons que je possède une application qui analyse un fichier XML, en crée un objet et stocke ces objets dans une base de données. Si la logique qui stocke les objets dans la base de données est connue pour fonctionner (comme dans: la société a besoin des données et n’est pas encore fictive), et même si cette logique est une grosse pile de merde, il n’existe imminent doit tester cela. Comme tout ce dont j'ai besoin pour être sûr que mon analyseur XML extrait les bonnes données. Je peux déduire de l’expérience que les bonnes données seront stockées.

Il existe des scénarios dans lesquels le test fonctionnel est très important, c’est-à-dire s’il fallait écrire une boutique en ligne. Dans ce cas, il serait essentiel que les articles achetés soient stockés dans la base de données. Dans ce cas, les tests fonctionnels avec la base de données de tests complète ont un sens absolu.

3
k0pernikus

Je n'ai jamais utilisé le PHPUnit_Extensions_Database_TestCase, principalement pour ces deux raisons:

  • Cela ne va pas bien. Si vous configurez et supprimez la base de données pour chaque test et que vous possédez une application qui s'appuie fortement sur la base de données, vous créez et supprimez le même schéma, encore et encore.
  • J'aime avoir mes appareils non seulement dans mes tests, mais également dans ma base de données de développement. Certains appareils sont même nécessaires à la production (utilisateur administrateur initial, catégories de produits ou autres). Les avoir dans un fichier xml qui ne peut être utilisé que par phpunit ne me semble pas bien.

Mon chemin en théorie ...

J'utilise le (doctrine/doctrine-fixtures-bundle } pour les fixtures (peu importe le but) et je configure toute la base de données avec tous les fixtures. J'exécute ensuite tous les tests sur cette base de données et veille à bien la recréer si un test l'a modifiée.

Les avantages sont que je n'ai pas besoin de reconfigurer une base de données si un test ne lit que sans rien changer. Pour les modifications, je dois le supprimer et le créer à nouveau ou m'assurer de revenir en arrière.

J'utilise sqlite pour les tests car je peux configurer la base de données, puis copier le fichier sqlite et le remplacer par un fichier vierge pour ramener la base de données d'origine. De cette façon, je n'ai pas besoin de supprimer la base de données, de la créer et de charger à nouveau tous les fixtures pour une base de données vierge. 

... et dans le code

J'ai écrit un article sur la façon dont je fais des tests de base de données avec symfony2 et phpunit }.

Bien qu’il utilise sqlite, je pense que l’on peut facilement apporter les modifications nécessaires pour utiliser MySQL, Postgres ou autre.

Penser plus loin

Voici d'autres idées qui pourraient fonctionner:

  • Une fois, j’ai lu un article sur une configuration de test où, avant d’utiliser la base de données, vous démarrez une transaction (dans la méthode setUp), puis vous utilisiez la fonction tearDown pour annuler. De cette façon, vous n'avez pas besoin de reconfigurer la base de données mais de l'initialiser une fois.
  • La configuration décrite ci-dessus a pour inconvénient que la base de données est configurée à chaque exécution de phpunit, même si vous n'exécutez que des tests unitaires sans interaction avec la base de données. J'expérimente avec une configuration dans laquelle j'utilise une variable globale qui indique si la base de données a été configurée, puis dans les tests, appelle une méthode qui vérifie cette variable et initialise la base de données si elle ne s'est pas encore produite. De cette façon, la configuration ne se produira que lorsque la base de données sera testée.
  • Un problème avec sqlite est qu’il ne fonctionne pas de la même manière que MySQL dans de rares cas. J'ai eu un problème une fois où quelque chose se comportait différemment dans MySQL et sqlite, ce qui entraînait l'échec d'un test lorsque tout fonctionnait avec MySQL. Je ne me souviens plus de ce que c'était exactement.
35
Sgoettschkes

Vous pouvez utiliser cette classe:

<?php

namespace Project\Bundle\Tests;

require_once dirname(__DIR__).'/../../../app/AppKernel.php';

use Doctrine\ORM\Tools\SchemaTool;

abstract class TestCase extends \PHPUnit_Framework_TestCase
{
/**
* @var Symfony\Component\HttpKernel\AppKernel
*/
protected $kernel;

/**
 * @var Doctrine\ORM\EntityManager
 */
protected $entityManager;

/**
 * @var Symfony\Component\DependencyInjection\Container
 */
protected $container;


public function setUp()
{
    // Boot the AppKernel in the test environment and with the debug.
    $this->kernel = new \AppKernel('test', true);
    $this->kernel->boot();

    // Store the container and the entity manager in test case properties
    $this->container = $this->kernel->getContainer();
    $this->entityManager = $this->container->get('doctrine')->getEntityManager();

    // Build the schema for sqlite
    $this->generateSchema();


    parent::setUp();
}

public function tearDown()
{
    // Shutdown the kernel.
    $this->kernel->shutdown();

    parent::tearDown();
}

protected function generateSchema()
{
    // Get the metadatas of the application to create the schema.
    $metadatas = $this->getMetadatas();

    if ( ! empty($metadatas)) {
        // Create SchemaTool
        $tool = new SchemaTool($this->entityManager);
        $tool->createSchema($metadatas);
    } else {
        throw new Doctrine\DBAL\Schema\SchemaException('No Metadata Classes to process.');
    }
}

/**
 * Overwrite this method to get specific metadatas.
 *
 * @return Array
 */
protected function getMetadatas()
{
    return $this->entityManager->getMetadataFactory()->getAllMetadata();
}
}

Et ensuite, vous pouvez tester votre entité. Quelque chose comme ça (en supposant que vous avez un utilisateur d'entité)

//Entity Test
class EntityTest extends TestCase {

    protected $user;

    public function setUp()
    {
         parent::setUp();
         $this->user = new User();
         $this->user->setUsername('username');
         $this->user->setPassword('p4ssw0rd');


         $this->entityManager->persist($this->user);
         $this->entityManager->flush();

    }

    public function testUser(){

         $this->assertEquals($this->user->getUserName(), "username");
         ...

    }

}

J'espère que cette aide.

Source: theodo.fr/blog/2011/09/symfony2-unit-database-tests

0
Munir