web-dev-qa-db-ger.com

Einrichten datenbankübergreifender Unit-Tests in Symfony2 mit PHPUnit

Ich bin in der Welt des Testens ziemlich neu und möchte sicherstellen, dass ich auf dem richtigen Weg bin.

Ich versuche, Unit-Tests in einem symfony2 -Projekt mit phpunit einzurichten. 

PHPUnit funktioniert und die einfachen Standard-Controller-Tests funktionieren einwandfrei. (Dabei geht es nicht um Funktionstests, sondern um Unit-Tests meiner Anwendung.)

Mein Projekt hängt jedoch stark von Datenbankinteraktionen ab. Soweit ich die Dokumentation von phpunit verstehe , sollte ich eine auf \PHPUnit_Extensions_Database_TestCase basierende Klasse einrichten, dann Fixtures für meine Datenbank erstellen und von dort aus arbeiten.

Symfony2 bietet jedoch nur eine WebTestCase-Klasse, die sich nur aus \PHPUnit_Framework_TestCase aus der Box heraus erstreckt.

Also kann ich davon ausgehen, dass ich meine eigene DataBaseTestCase erstellen sollte, die meistens WebTestCase kopiert, mit dem Unterschied, dass sie sich von \PHPUnit_Extensions_Database_TestCase erstreckt und alle ihre abstrakten Methoden implementiert?

Oder gibt es einen anderen "eingebauten" empfohlenen Workflow für symfony2, der datenbankzentrierte Tests betrifft?

Da ich sicherstellen möchte, dass meine Modelle die richtigen Daten speichern und abrufen, möchte ich nicht die Besonderheiten von doctrine aus Versehen testen.

43
k0pernikus

tl; dr:

  • Wenn und nur wenn Sie die gesamte Funktionsteststrecke gehen wollen, empfehle ich, die Antwort von Sgoettschkes nachzuschlagen.
  • Wenn Sie Ihre Anwendung als Komponententest testen möchten und Code testen möchten, der mit der Datenbank interagiert, lesen Sie entweder weiter oder springen Sie direkt zu symfony2 docs


In meiner ursprünglichen Frage gab es bestimmte Aspekte, die deutlich machen, dass mir die Unterschiede zwischen den Komponententests und den Funktionstests nicht klar waren. (Wie ich geschrieben habe, möchte ich die Anwendung als Unit-Test testen, sprach aber gleichzeitig auch über Controller-Test; und das sind Funktionstests per Definition).

Unit-Tests sind nur für Services und nicht für Repositorys sinnvoll. Diese Services können Mocks des Entity Managers verwenden. (Ich würde sogar so weit gehen zu sagen: Wenn möglich, schreiben Sie Services, bei denen nur Entitäten erwartet werden. Dann müssen Sie nur Mocks dieser Entitäten erstellen, und Ihre Unit-Tests Ihrer Geschäftslogik werden sehr einfach.)

Mein tatsächlicher Anwendungsfall für meine Anwendung spiegelte sich in den symfony2-Dokumenten auf so wider, wie Code getestet wird, der mit der Datenbank interagiert .

Sie bieten dieses Beispiel für einen Servicetest:

Serviceklasse:

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();
    }
}

Servicetestklasse:

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));
    }
}

Für diese Art von Test ist keine Testdatenbank erforderlich, nur Spott.

Es ist wichtig, die Geschäftslogik zu testen, nicht die Persistenzschicht.

Nur für Funktionstests ist es sinnvoll, eine eigene Testdatenbank zu haben, die anschließend erstellt und abgebaut werden soll. Die große Frage sollte sein: 

Wann sind Funktionstests sinnvoll?

Früher dachte ich, dass teste alle Dinge die richtige Antwort ist; Nachdem ich jedoch mit einer Menge von Altsoftware gearbeitet habe, die an sich kaum testgetrieben war, bin ich etwas mehr geworden faulpragmatisch und betrachten bestimmte Funktionen als funktionsfähig, bis sie durch einen Fehler nachgewiesen werden.

Angenommen, ich habe eine Anwendung, die ein XML-Objekt analysiert, daraus ein Objekt erstellt und diese Objekte in einer Datenbank speichert. Wenn bekannt ist, dass die Logik, mit der die Objekte in der Datenbank gespeichert sind, funktioniert (wie in: Das Unternehmen benötigt die Daten und ist bis jetzt noch nicht defekt), und selbst wenn diese Logik ein großer hässlicher Misthaufen ist, gibt es keine amminent muss das testen. Ich muss also sicherstellen, dass mein XML-Parser die richtigen Daten extrahiert. Ich kann aus Erfahrung schließen, dass die richtigen Daten gespeichert werden.

Es gibt Szenarien, in denen Funktionstests ziemlich wichtig sind, d. H. Wenn man einen Online-Shop schreiben würde. Hier wäre es geschäftskritisch, dass gekaufte Artikel in der Datenbank abgelegt werden, und hier sind Funktionstests mit der gesamten Testdatenbank absolut sinnvoll.

3
k0pernikus

Ich habe den PHPUnit_Extensions_Database_TestCase noch nie verwendet, hauptsächlich aus zwei Gründen:

  • Es skaliert nicht gut. Wenn Sie die Datenbank für jeden einzelnen Test einrichten und abbauen und Sie über eine Anwendung verfügen, die stark von der Datenbank abhängt, müssen Sie dasselbe Schema immer wieder erstellen und löschen.
  • Ich mag es, meine Fixtures nicht nur in meinen Tests, sondern auch in meiner Entwicklungsdatenbank zu haben. Einige Fixtures werden sogar für die Produktion benötigt (anfänglicher Administrator, Produktkategorien oder was auch immer). Sie in einem XML-Code zu speichern, der nur für Phpunit verwendet werden kann, erscheint mir nicht richtig.

Mein theoretischer Weg ...

Ich benutze das Doctrine/Doctrine-Fixtures-Bundle für Fixtures (egal zu welchem ​​Zweck) und richte die gesamte Datenbank mit allen Fixtures ein. Ich führe dann alle Tests für diese Datenbank aus und stelle sicher, dass die Datenbank neu erstellt wird, wenn ein Test sie geändert hat.

Der Vorteil ist, dass ich keine Datenbank erneut einrichten muss, wenn ein Test nur liest, aber nichts ändert. Für Änderungen muss ich es ablegen und neu erstellen oder sicherstellen, dass die Änderungen rückgängig gemacht werden.

Ich benutze sqlite zum Testen, weil ich die Datenbank einrichten kann, dann die sqlite-Datei kopieren und durch eine saubere ersetzen kann, um die ursprüngliche Datenbank wiederherzustellen. Auf diese Weise muss ich die Datenbank nicht löschen, erstellen und alle Fixtures erneut laden, um eine saubere Datenbank zu erhalten. 

... und im Code

Ich habe einen Artikel darüber geschrieben, wie ich Datenbank-Tests mit symfony2 und phpunit durchführe.

Obwohl es sqlite verwendet, denke ich, dass man die Änderungen leicht machen kann, um MySQL oder Postgres oder was auch immer zu verwenden.

Weiterdenken

Hier sind einige andere Ideen, die funktionieren könnten:

  • Ich habe einmal von einem Test-Setup gelesen, in dem Sie vor der Verwendung der Datenbank eine Transaktion (innerhalb der Methode setUp) starten und dann mit dem tearDown-Befehl zurückrollen. Auf diese Weise müssen Sie die Datenbank nicht erneut einrichten und müssen sie nur einmal initialisieren.
  • Mein oben beschriebenes Setup hat den Nachteil, dass die Datenbank bei jeder Ausführung von phpunit eingerichtet wird, auch wenn Sie nur einige Komponententests ohne Datenbankinteraktion ausführen. Ich experimentiere mit einem Setup, bei dem ich eine globale Variable verwende, die angibt, ob die Datenbank eingerichtet wurde, und dann innerhalb der Tests eine Methode aufrufen, die diese Variable prüft und die Datenbank initialisiert, falls dies noch nicht geschehen ist. Auf diese Weise würde das Setup nur dann durchgeführt werden, wenn ein Test die Datenbank benötigt.
  • Ein Problem mit sqlite ist, dass es in einigen seltenen Fällen nicht genauso funktioniert wie MySQL. Ich hatte einmal ein Problem, bei dem sich etwas in MySQL und sqlite anders verhielt, was dazu führte, dass ein Test fehlschlug, wenn in MySQL alles funktionierte. Ich kann mich nicht erinnern, was genau war.
35
Sgoettschkes

Sie können diese Klasse verwenden:

<?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();
}
}

Und dann können Sie Ihre Entität testen. So etwas (vorausgesetzt, Sie haben eine Entität User)

//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");
         ...

    }

}

Ich hoffe das hilft.

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

0
Munir