web-dev-qa-db-fra.com

Comment écrire des tests unitaires en PHP?

J'ai lu partout à quel point ils sont excellents, mais pour une raison quelconque, je n'arrive pas à comprendre à quel point je suis censé tester quelque chose. Quelqu'un pourrait-il publier un exemple de code et comment le tester? Si ce n'est pas trop de problème :)

93
letgo

Il existe un 3ème "framework", qui est de loin plus facile à apprendre - encore plus facile que Simple Test, ça s'appelle phpt.

Une introduction peut être trouvée ici: http://qa.php.net/write-test.php

Edit: Je viens de voir votre demande d'exemple de code.

Supposons que vous ayez la fonction suivante dans un fichier appelé lib.php:

<?php
function foo($bar)
{
  return $bar;
}
?>

Vraiment simple et direct, le paramètre que vous transmettez est retourné. Examinons donc un test pour cette fonction, nous appellerons le fichier de test foo.phpt:

--TEST--
foo() function - A basic test to see if it works. :)
--FILE--
<?php
include 'lib.php'; // might need to adjust path if not in the same dir
$bar = 'Hello World';
var_dump(foo($bar));
?>
--EXPECT--
string(11) "Hello World"

En résumé, nous fournissons le paramètre $bar Avec la valeur "Hello World" Et nous var_dump() la réponse de l'appel de fonction à foo().

Pour exécuter ce test, utilisez: pear run-test path/to/foo.phpt

Cela nécessite ne installation fonctionnelle de PEAR sur votre système, ce qui est assez courant dans la plupart des cas. Si vous devez l'installer, je vous recommande d'installer la dernière version disponible. Si vous avez besoin d'aide pour le configurer, n'hésitez pas à demander (mais fournissez le système d'exploitation, etc.).

36
Till

Il existe deux cadres que vous pouvez utiliser pour les tests unitaires. Simpletest et PHPUnit , que je préfère. Lisez les tutoriels sur la façon d'écrire et d'exécuter des tests sur la page d'accueil de PHPUnit. C'est assez simple et bien décrit.

29
okoman

Vous pouvez rendre les tests unitaires plus efficaces en modifiant votre style de codage pour l'adapter.

Je recommande de parcourir le Google Testing Blog , en particulier le post sur Writing Testable Code .

21
Preston

J'ai roulé le mien parce que je n'ai pas eu le temps d'apprendre à quelqu'un d'autre à faire les choses, cela a pris environ 20 minutes à rédiger, 10 à l'adapter pour le poster ici.

Unittesting est très utile pour moi.

c'est un peu long mais ça s'explique et il y a un exemple en bas.

/**
 * Provides Assertions
 **/
class Assert
{
    public static function AreEqual( $a, $b )
    {
        if ( $a != $b )
        {
            throw new Exception( 'Subjects are not equal.' );
        }
    }
}

/**
 * Provides a loggable entity with information on a test and how it executed
 **/
class TestResult
{
    protected $_testableInstance = null;

    protected $_isSuccess = false;
    public function getSuccess()
    {
        return $this->_isSuccess;
    }

    protected $_output = '';
    public function getOutput()
    {
        return $_output;
    }
    public function setOutput( $value )
    {
        $_output = $value;
    }

    protected $_test = null;
    public function getTest()
    {
        return $this->_test;
    }

    public function getName()
    {
        return $this->_test->getName();
    }
    public function getComment()
    {
        return $this->ParseComment( $this->_test->getDocComment() );
    }

    private function ParseComment( $comment )
    {
        $lines = explode( "\n", $comment );
        for( $i = 0; $i < count( $lines ); $i ++ )
        {
            $lines[$i] = trim( $lines[ $i ] );
        }
        return implode( "\n", $lines );
    }

    protected $_exception = null;
    public function getException()
    {
        return $this->_exception;
    }

    static public function CreateFailure( Testable $object, ReflectionMethod $test, Exception $exception )
    {
        $result = new self();
        $result->_isSuccess = false;
        $result->testableInstance = $object;
        $result->_test = $test;
        $result->_exception = $exception;

        return $result;
    }
    static public function CreateSuccess( Testable $object, ReflectionMethod $test )
    {
        $result = new self();
        $result->_isSuccess = true;
        $result->testableInstance = $object;
        $result->_test = $test;

        return $result;
    }
}

/**
 * Provides a base class to derive tests from
 **/
abstract class Testable
{
    protected $test_log = array();

    /**
     * Logs the result of a test. keeps track of results for later inspection, Overridable to log elsewhere.
     **/
    protected function Log( TestResult $result )
    {
        $this->test_log[] = $result;

        printf( "Test: %s was a %s %s\n"
            ,$result->getName()
            ,$result->getSuccess() ? 'success' : 'failure'
            ,$result->getSuccess() ? '' : sprintf( "\n%s (lines:%d-%d; file:%s)"
                ,$result->getComment()
                ,$result->getTest()->getStartLine()
                ,$result->getTest()->getEndLine()
                ,$result->getTest()->getFileName()
                )
            );

    }
    final public function RunTests()
    {
        $class = new ReflectionClass( $this );
        foreach( $class->GetMethods() as $method )
        {
            $methodname = $method->getName();
            if ( strlen( $methodname ) > 4 && substr( $methodname, 0, 4 ) == 'Test' )
            {
                ob_start();
                try
                {
                    $this->$methodname();
                    $result = TestResult::CreateSuccess( $this, $method );
                }
                catch( Exception $ex )
                {
                    $result = TestResult::CreateFailure( $this, $method, $ex );
                }
                $output = ob_get_clean();
                $result->setOutput( $output );
                $this->Log( $result );
            }
        }
    }
}

/**
 * a simple Test suite with two tests
 **/
class MyTest extends Testable
{
    /**
     * This test is designed to fail
     **/
    public function TestOne()
    {
        Assert::AreEqual( 1, 2 );
    }

    /**
     * This test is designed to succeed
     **/
    public function TestTwo()
    {
        Assert::AreEqual( 1, 1 );
    }
}

// this is how to use it.
$test = new MyTest();
$test->RunTests();

Cela produit:

Test: TestOne était un échec 
/** 
 * Ce test est conçu pour échouer 
 **/(lignes: 149-152; fichier:/Users/kris/Desktop/Testable.php) 
 Test: TestTwo a été un succès 
12
Kris

Obtenez PHPUnit. C'est très simple à utiliser.

Commencez ensuite par des affirmations très simples. Vous pouvez faire beaucoup avec AssertEquals avant de vous lancer dans autre chose. C'est un bon moyen de se mouiller les pieds.

Vous pouvez également essayer d'écrire votre test en premier (puisque vous avez donné à votre question la balise TDD), puis écrire votre code. Si vous ne l'avez pas fait auparavant, cela vous ouvre les yeux.

require_once 'ClassYouWantToTest';
require_once 'PHPUnit...blah,blah,whatever';

class ClassYouWantToTest extends PHPUnit...blah,blah,whatever
{
    private $ClassYouWantToTest;

   protected function setUp ()
    {
        parent::setUp();
        $this->ClassYouWantToTest = new ClassYouWantToTest(/* parameters */);
    }

    protected function tearDown ()
    {
        $this->ClassYouWantToTest = null;
        parent::tearDown();
    }

    public function __construct ()
    {   
        // not really needed
    }

    /**
     * Tests ClassYouWantToTest->methodFoo()
     */
    public function testMethodFoo ()
    {
        $this->assertEquals(
            $this->ClassYouWantToTest->methodFoo('putValueOfParamHere), 'expectedOutputHere);

    /**
     * Tests ClassYouWantToTest->methodBar()
     */
    public function testMethodFoo ()
    {
        $this->assertEquals(
            $this->ClassYouWantToTest->methodBar('putValueOfParamHere), 'expectedOutputHere);
}
6
PartialOrder

Pour les tests ET la documentation simples, php-doctest est assez agréable et c'est un moyen très simple de commencer car vous n'avez pas besoin d'ouvrir un fichier séparé. Imaginez la fonction ci-dessous:

/**
* Sums 2 numbers
* <code>
* //doctest: add
* echo add(5,2);
* //expects:
* 7
* </code>
*/
function add($a,$b){
    return $a + $b;   
}

Si vous exécutez maintenant ce fichier via phpdt (coureur de ligne de commande de php-doctest), 1 test sera exécuté. Le doctest est contenu dans le bloc <code>. Doctest est originaire de python et est parfait pour donner des exemples utiles et exécutables sur la façon dont le code est censé fonctionner. Vous ne pouvez pas l'utiliser exclusivement parce que le code lui-même joncherait de cas de test mais je 'ai trouvé qu'il était utile à côté d'une bibliothèque tdd plus formelle - j'utilise phpunit.

Cette 1ère réponse ici résume bien (ce n'est pas l'unité vs le doctorat).

5
Sofia

Les tests de codéception ressemblent beaucoup aux tests unitaires courants, mais sont beaucoup plus puissants dans les cas où vous avez besoin de moqueries et de tronçonnage.

Voici l'exemple de test du contrôleur. Notez la facilité avec laquelle les stubs sont créés. La facilité avec laquelle vous vérifiez la méthode a été invoquée.

<?php
use Codeception\Util\Stub as Stub;

const VALID_USER_ID = 1;
const INVALID_USER_ID = 0;

class UserControllerCest {
public $class = 'UserController';


public function show(CodeGuy $I) {
    // prepare environment
    $I->haveFakeClass($controller = Stub::makeEmptyExcept($this->class, 'show'));
    $I->haveFakeClass($db = Stub::make('DbConnector', array('find' => function($id) { return $id == VALID_USER_ID ? new User() : null ))); };
    $I->setProperty($controller, 'db', $db);

    $I->executeTestedMethodOn($controller, VALID_USER_ID)
        ->seeResultEquals(true)
        ->seeMethodInvoked($controller, 'render');

    $I->expect('it will render 404 page for non existent user')
        ->executeTestedMethodOn($controller, INVALID_USER_ID)
        ->seeResultNotEquals(true)
        ->seeMethodInvoked($controller, 'render404','User not found')
        ->seeMethodNotInvoked($controller, 'render');
}
}

Il y a aussi d'autres choses sympas. Vous pouvez tester l'état de la base de données, le système de fichiers, etc.

2
Davert

phpunit est à peu près le cadre de test unitaire de facto pour php. il y a aussi DocTest (disponible en tant que PEAR) et quelques autres. php lui-même est testé pour les régressions et similaires via tests phpt = qui peut également être exécuté via poire.

2
kguest

Outre les excellentes suggestions sur les frameworks de test déjà données, construisez-vous votre application avec l'un des frameworks Web PHP qui ont des tests automatisés intégrés, tels que Symfony ou - CakePHP ? Parfois, avoir un endroit où déposer vos méthodes de test réduit le frottement au démarrage que certaines personnes associent aux tests automatisés et au TDD.

1
bradheintz

Beaucoup trop pour re-poster ici, mais voici un excellent article sur l'utilisation de phpt . Il couvre un certain nombre d'aspects autour de phpt qui sont souvent négligés, donc cela pourrait valoir la peine d'être lu pour élargir vos connaissances en php au-delà de la simple écriture d'un test. Heureusement, l'article traite également des tests d'écriture!

Les principaux points de discussion

  1. Découvrez comment des aspects marginalement documentés de PHP fonctionnent (ou à peu près n'importe quelle partie d'ailleurs)
  2. Écrivez des tests unitaires simples pour votre propre PHP
  3. Écrire des tests dans le cadre d'une extension ou pour transmettre un bug potentiel aux internes ou aux groupes d'assurance qualité
1
quickshiftin

Je sais qu'il y a déjà beaucoup d'informations ici, mais comme cela apparaît toujours dans les recherches Google, je pourrais aussi bien ajouter Chinook Test Suite à la liste. Il s'agit d'un cadre de test simple et petit.

Vous pouvez facilement tester vos classes avec lui et également créer des objets fictifs. Vous exécutez les tests via un navigateur Web et (pas encore) via une console. Dans le navigateur, vous pouvez spécifier quelle classe de test ou même quelle méthode de test exécuter. Ou vous pouvez simplement exécuter tous les tests.

Une capture d'écran de la page github:

Chinook Unit Test framework

Ce que j'aime, c'est la façon dont vous affirmez les tests. Cela se fait avec des "assertions fluentes". Exemple:

$this->Assert($datetime)->Should()->BeAfter($someDatetime);

Et la création d'objets fictifs est également un jeu d'enfant (avec une syntaxe fluide):

$mock = new CFMock::Create(new DummyClass());
$mock->ACallTo('SomeMethod')->Returns('some value');

Quoi qu'il en soit, plus d'informations peuvent être trouvées sur la page github avec un exemple de code:

https://github.com/w00/Chinook-TestSuite

1
Vivendi