Ecrire des tests unitaires PHP

This page is a translated version of the page Manual:PHP unit testing/Writing unit tests and the translation is 98% complete.
Outdated translations are marked like this.

Conseils généraux

Le manuel de tests unitaires PHP fournit de bonnes informations quant à la compréhension et le développement des tests unitaires. Faites particulièrement attention aux sections concernant l'écriture et l'organisation des tests.

Développer les meilleures pratiques

Les développeurs nouveaux sur les tests unitaires de MediaWiki doivent utiliser SampleTest.php comme point d'entrée – il contient des commentaires utiles facilitant le processus d'apprentissage de l'écriture des tests unitaires.

Une autre ressource sont les transparents de la discussion sur les Meilleures pratiques de PHPUnit que Sebastian Bergmann a donnée à l'OSCON 2010.

Les développeurs doivent éviter d'inventer de nouvelles conventions, ou d'importer les conventions d'autres environnements; l'utilisation des conventions PHPUnit déja bien établies sert à ce que les tests MediaWiki soient à la fois utiles et utilisables.

Les tests unitaires doivent suivre l' Ensemble des règles des tests unitaires de Michael Feathers.

Ecrire du code testable

Veuillez écrire du code qui soit testable.

MediaWiki n'a pas été écrit avec l'objectif d'être testable. Il utilise des variables globales partout et des méthodes statiques à de nombreux endroits. C'est un héritage que nous devons accepter, mais n'introduisez pas ces éléments dans du nouveau code et essayez de modifier en avançant.

Une bonne ressource peut être le Guide de la testabilité de Miško Hevery. (Miško Hevery est l'un [?] des coachs Agile de Google).

Conventions des tests

Le nom du fichier doit se terminer par Test.php. Utilisez SampleTest.php comme point d'entrée.

setUp() et tearDown()

  • Doivent être des fonctions de type protected.
  • tearDown() doit être dans l'ordre inverse de setUp().
  • setUp() commence par appeler son parent.
  • tearDown() se termine en appelant son parent.

Fonctions d'assertions

  • Doivent être des fonctions de type public.
  • Le nom de la fonction doit être de la forme lowerCamelCase où la première lettre est en bas de casse, et commencer par le mot test; par exemple, function testFooBar.
  • Partout où cela est possible, faire référence à la méthode la plus importante à tester; par exemple, Html::expandAttributes est testé dans HtmlTest::testExpandAttributes.

Sources de données

  • Doivent être des fonctions de type public.
  • Le nom du fournisseur de données doit être de la forme lowerCamelCase où la première lettre est en bas de casse, et commencer par le mot provide; par exemple, provideHtml5InputTypes.
  • Ne pas instancier les services MediaWiki, car les fournisseurs de données sont appelés avant setUpBeforeClass. Par exemple au lieu de Title::newFromText, utiliser new TitleValue pour éviter de créer un TitleParser.

Raconter une histoire

Les sorties des tests doivent raconter une histoire. Le format de sortie --testdox est une bonne manière de visualiser cette histoire : l'exécution d'une suite de tests est affichée comme un ensemble de déclarations à propos des classes de tests, avec l'indication pour chacune d'elle si elle a réussi ou pas. Les déclarations (à moins d'être adaptées) sont les noms des méthodes de test avec la casse et les espaces corrigés.

L'annotation @testdox peut être utilisée pour adapter le message à afficher. Actuellement, ceci n'est pas utilisé dans la base de code MediaWiki.

Voir les Autres utilisations pour les tests pour plus d'informations.

Nombre d'assertions

Chaque test ne doit vérifier qu'une seule assertion, sauf s'il existe une bonne raison (regrouper les tests longs).

Cas d'échec

Habituellement le code de test ne doit pas exécuter die("Error") mais utiliser la méthode fail de phpunit :

$this->fail( 'A useful error message' )

Ceci devrait apparaître comme un incident dans le résumé de test plutôt que de bloquer toute la suite de tests.

Spécifique à MediaWiki

Regrouper les tests

PHPUnit permet de regrouper les tests dans des groupes arbitraires. Les groupes de tests peuvent être sélectionnés pour être exécutés, ou exclus de l'exécution, lorsque la suite de tests est lancée (voir l'Annotation @group, le Lanceur de tests en mode ligne de commande et la documentation du Fichier de configuration XML dans le manuel PHPUnit pour les détails complémentaires).

Pour ajouter un test (ou une classe) à un groupe, utiliser l'annotation @group dans la partie docblock qui précède le code. Par exemple :

/**
 * @group Broken
 * @group Internationalization
 */
class HttpTest extends MediaWikiTestCase {
    ...

Plusieurs groupes fonctionnels sont actuellement utilisés dans les tests unitaires MediaWiki :

  • API – Tests impliquant l'API MediaWiki.
  • Broken – Place les tests en échec dans le groupe Broken. Les tests de ce groupe ne seront pas exécutés (conformément à ce qui est configuré dans tests/phpunit/suite.xml).
  • Database – Les tests qui nécessitent une connexion à la base de données doivent être mis dans le groupe Database.
Ceci fait que les tables temporaires sont réécrasées par rapport aux tables réelles de la base de données pour que les cas de tests puissent réaliser des opérations sur la base de données sans modifier le wiki actuel.
  • Destructive – Les tests qui modifient ou qui détruisent des données doivent être placés dans le groupe Destructive.
  • Search – Les tests qui utilisent le search intégré de MediaWiki vont dans le groupe Search.
  • SeleniumFrameworkLes tests qui nécessitent que SeleniumFramework soit installé doivent être mis dans le groupe SeleniumFramework.
  • Stub – Placez les bouchons des tests dans le groupe Stub. Les tests de ce groupe ne seront pas exécutés (conformément à ce qui est configuré dans tests/phpunit/suite.xml).
  • Sqlite – Les tests qui utilisent SQLite doivent être mis dans le groupe Sqlite.
  • Standalone – Tests très lents qui ne doivent pas s'exécuter dans le share gate job, pour permettre d'exécuter plus rapidement les autres tests de l'intégration continue.
  • Upload – Les tests qui téléversent des fichiers doivent être mis dans le groupe Upload.
  • Utility – N'est pas utilisé actuellement par aucun test. Les tests de ce groupe ne seront pas exécutés (conformément à ce qui est configuré dans tests/phpunit/suite.xml).

En plus, vous pouvez regrouper les tests en fonction de l'équipe de développement :

  • Fundraising pour les levées de fonds
  • EditorEngagement pour l'engagement du contributeur
  • Internationalization pour les traductions
  • etc.

Pour tester simplement un groupe particulier, utiliser l'option --group à partir de la ligne de commande.

composer phpunit:entrypoint -- --group Search

ou si vous passez par le Makefile de core/tests/phpunit :

make FLAGS="--group Search" target

où la cible peut être : phpunit, safe, etc.

Couverture de test

La Documentation PHPUnit possède un chapître à propos de la couverture de test. Un rapport de couverture pour le noyau MediaWiki est généré deux fois par jour. Comme l'option forceCoversAnnotation doit être activée, le test doit être balisé avec des annotations @covers pour identifier les parties de code à tester (à la différence du code exécuté simplement et dont les résultats ne sont jamais testés avec des assertions),

Notez que @covers demande à avoir des noms de classes qui soient complètement qualifiés (à la différence des annotations Doxygen telles que @param).

Classe

Il est possible d'étendre une classe de test MediaWiki.

class HttpTest extends MediaWikiTestCase {
    ...

Vous trouverez ci-après les classes de test MediaWiki que vous pouvez étendre. Les puces de niveau inférieur étendent leurs parents.

  • TestCase – classe de test de PHPUnit
    • MediaWikiUnitTestCase – Pour les tests unitaires des méthodes sans dépendance, ou des méthodes dont les dépendances sont complètement bouchonnées. Ils doivent aller dans leur propre sous-répertoire appelé /unit/ pour que phpunit:unit fonctionne correctement.
      • HookRunnerTestBase – Teste que chaque argument passé dans une classe HookRunner et passé avec un HookContainer.
    • MediaWikiIntegrationTestCase – Aide en fournissant des classes de test qui accèdent aux variables globales, aux méthodes, aux services, ou à un serveur de stockage. Empêche d'envoyer les courriels acuels. Ils doivent aller dans leur propre sous-répertoire appelé /integration/ de sorte que phpunit:integration fonctionne correctement. Peut tester en sécurité la base de données SQL si vous ajoutez @group Database à vos tests. Les modifications de la base de données sont annulées avant chaque test de méthode.
      • MediaWikiLangTestCase – Réalise certaines configurations de la langue comme $this->setUserLang( 'en' ); et $this->setContentLang( 'en' ); et exécute $services->getMessageCache()->disable()
        • ApiTestCase –- Comporte certaines méthodes supplémentaires pour tester Action API telles que doApiRequest(), doApiRequestWithToken(), buildFauxRequest(), etc.
      • MaintenanceBaseTestCase – Pour le test des classes de maintenance MediaWiki.
      • SpecialPageTestBase – Pour tester les pages spéciales de MediaWiki.

Les bases de données

Si vous testez du code qui dépend de la base de données, vous devez mettre votre cas de test dans le groupe Database (voir ci-dessus). Cela indique à MediaWikiTestCase d'établir une connection avec la base de données DB_PRIMARY que vous utiliserez dans $this->db. Normalement, ceci utilise une base de données temporaire et distincte, avec des données limitées préremplies par addCoreDBData, comprenant un utilisateur 'UTSysop' et un titre 'UTPage'. (La base de données temporaire est créée avec CREATE TEMPORARY TABLE et préfixée avec unittest_). Vous pouvez forcer PHPUnit à ne pas utiliser les tables temporaires (par exemple, si vous voulez déboguer et regarder dans la base de données à l'aide d'un afficheur de base de données) en définissant la variable .env PHPUNIT_USE_NORMAL_TABLES=1. Un cas de test peut ajouter des données supplémentaires à la base de données en réécrasant addDBData (qui par défaut ne fait rien).

Néanmoins il est à noter que seul le schéma des tables est recopié à partir de votre base de données actuelle et non pas les données qu'elles contiennent.

Vous pouvez directement tester le contenu actuel de la base de données avec assertSelect().

$this->assertSelect(
	'test', // table
	[ 'first_name', 'last_name', 'street' ], // champs à sélectionner
	[], // conditions
	[ [ 'Jane', 'Doe', 'Broadway' ] ] // valeurs attendues
);

Voici quelques exemples d'extensions que vous pouvez regarder à titre de référence.

Les tests qui ne se trouvent pas dans le groupe Database sont encore exécutés avec une base de données clonée et temporaire (même s'ils ignorent $this->db et à la place utilisent par exemple wfGetDB() directement); néanmoins cette base n'est configurée qu'une seule fois pour toute la séquence de test et n'est pas réinitialisée entre les tests. Les tests doivent éviter de s'appuyer sur cette fonctionnalité de sécurité autant que possible.

Scripts de maintenance

Les cas de test pour les maintenance scripts doivent hériter de MediaWiki\Tests\Maintenance\MaintenanceBaseTestCase, pour gérer les différents canaux de sortie utilisés par les scripts de maintenance.

La méthode setUp() du cas de test de base va instancier pour vous, votre objet Maintenance si vous indiquez la classe à construire en fournissant le getMaintenanceClass() obligatoire dans votre sous-classe :

    public function getMaintenanceClass() {
        return PurgeScoreCache::class;
    }

Dans le cas improbable où vous voudriez faire quelque chose de spécial pour instancier la classe à tester, vous pouvez réécraser la méthode createMaintenance(), mais cela n'est heureusement pas nécessaire.

Par défaut, la sortie des scripts de maintenance sera supprimée et ignorée. Si vous souhaitez tester la sortie (et c'est une bonne idée), utilisez un code tel que :

use MediaWiki\Tests\Maintenance\MaintenanceBaseTestCase;

class PurgeScoreCacheTest extends MaintenanceBaseTestCase {
    public function testNotAThing() {
        $this->maintenance->loadWithArgv( [ '--model', 'not_a_thing' ] );
        $this->maintenance->execute();

        $this->expectOutputRegex( '/skipping \'not_a_thing\' model/' );
    }
}