doctrine2 tutorial - Comment configurer des tests unitaires de base de données dans Symfony2 en utilisant PHPUnit?




openclassroom framework (4)

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

J'essaie de mettre en place des tests unitaires dans un projet symfony2 en utilisant phpunit .

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

Mon projet repose cependant fortement sur des interactions de base de données, et si je comprends bien de la documentation de phpunit , je devrais mettre en place 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 ne propose qu'une classe WebTestCase qui ne s'étend que depuis \PHPUnit_Framework_TestCase .

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

Ou y a-t-il un autre workflow "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 détails de la doctrine par accident.


Answers

tl; dr:

  • Si et seulement si vous voulez faire tout le parcours d'essai fonctionnel, alors je vous recommande de rechercher la réponse de Sgoettschkes .
  • Si vous voulez tester votre application unitaire et tester un code qui interagit avec la base de données, lisez-le ou accédez directement à la documentation de symfony2

Certains aspects de ma question initiale indiquaient clairement que je ne comprenais pas les différences entre les tests unitaires et les tests fonctionnels. (Comme je l'ai écrit je veux tester l'application unitaire, mais parlait aussi de test du contrôleur en même temps, et ce sont des tests fonctionnels par définition).

Les tests unitaires n'ont de sens que pour les services et non pour les référentiels. Et ces services peuvent utiliser des simulacres du gestionnaire d'entités. (J'irais même jusqu'à dire: Si possible, écrivez des services qui attendent seulement que des entités leur soient transmises, ensuite il vous suffit de 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 reflété sur les documents symfony2 sur la façon de 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 genre de test, seulement moqueur.

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

Seulement pour les tests fonctionnels est-il logique d'avoir sa propre base de données de test que l'on devrait construire et démonter ensuite, et la grande question devrait être:

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

Je pensais que tester toutes les choses est la bonne réponse; Pourtant, après avoir travaillé avec beaucoup de logiciels hérités qui étaient en eux-mêmes à peine testés, je suis devenu un peu plus paresseux pragmatique et considère certaines fonctionnalités comme fonctionnant jusqu'à preuve du contraire par un bug.

Supposons que j'ai une application qui analyse un 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é nécessite les données et est, pour l'instant, pas cassé), et même si cette logique est un gros tas de merde laid, il n'y a pas besoin imminent de tester cela. Comme tout ce dont j'ai besoin pour m'assurer 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 où les tests fonctionnels sont très importants, c'est-à-dire si l'on devait écrire une boutique en ligne. Là, il serait essentiel que les articles achetés soient stockés dans la base de données et ici, les tests fonctionnels avec toute la base de données de test ont un sens absolu.


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 puis vous pouvez tester votre entité. Quelque chose comme ceci (en supposant que vous avez une entité Utilisateur)

//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


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

  • Ça ne va pas bien. Si vous configurez et supprimez la base de données pour chaque test et que vous disposez d'une application qui dépend fortement de la base de données, vous créez et supprimez le même schéma encore et encore.
  • J'aime avoir mes montages non seulement dans mes tests mais aussi dans ma base de données de développement et certains appareils sont même nécessaires pour la production (utilisateur admin initial ou catégories de produits ou autre). Les avoir à l'intérieur d'un xml qui ne peut être utilisé que pour phpunit ne me semble pas correct.

Mon chemin en théorie ...

J'utilise le doctrine/doctrine-fixtures-bundle pour les appareils (peu importe le but) et configure la base de données entière avec tous les appareils. J'exécute alors tous les tests contre cette base de données et m'assure de recréer la base de données si un test l'a changé.

Les avantages sont que je n'ai pas besoin de configurer une base de données si un test ne lit que mais ne change rien. Pour les changements que je dois faire, déposez-le et créez-le à nouveau ou assurez-vous de rétablir les changements.

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

... et dans le code

J'ai écrit un article sur comment 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 faire les changements pour utiliser MySQL ou Postgres ou autre.

Penser plus loin

Voici quelques autres idées qui pourraient fonctionner:

  • J'ai lu une fois sur une installation de test où avant d'utiliser la base de données, vous démarrez une transaction (dans la méthode setUp), puis utilisez le tearDown pour restaurer. De cette façon, vous n'avez plus besoin de configurer la base de données et vous avez juste besoin de l'initialiser une fois.
  • Ma configuration décrite ci-dessus présente l'inconvénient que la base de données est configurée chaque fois que phpunit est exécuté, même si vous exécutez uniquement des tests unitaires sans interaction avec la base de données. J'expérimente avec une configuration où j'utilise une variable globale qui indique si la base de données a été installée et 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 seulement quand un test a besoin de la base de données, la configuration se produira.
  • Un problème avec sqlite est qu'il ne fonctionne pas comme MySQL dans de rares cas. J'ai eu un problème une fois où quelque chose se comportait différemment dans MySQL et sqlite provoquant l'échec d'un test quand MySQL fonctionnait. Je ne me souviens pas de ce que c'était exactement.

Ajoutez-le en tant que bibliothèque ... dans le projet que vous modifiez, ajoutez-le à 'Bibliothèques externes'.

Il devrait ensuite être inclus.







php symfony doctrine2 phpunit