web-dev-qa-db-fra.com

Comment utiliser PHPUnit avec CodeIgniter?

J'ai lu et lu des articles sur PHPUnit, SimpleTest et d'autres frameworks de tests unitaires. Ils sonnent tous tellement bien! J'ai finalement réussi à faire fonctionner PHPUnit avec Codeigniter grâce à https://bitbucket.org/kenjis/my-ciunit/overview

Maintenant, ma question est, comment puis-je l'utiliser?

Chaque tutoriel que je vois a une utilisation abstraite comme assertEquals(2, 1+1) ou:

public function testSpeakWithParams()
{
    $hello = new SayHello('Marco');
    $this->assertEquals("Hello Marco!", $hello->speak());
}

C'est super si j'avais une fonction qui produirait une chaîne aussi prévisible. Habituellement, mes applications récupèrent un tas de données de la base de données, puis les affichent dans une sorte de tableau. Alors, comment puis-je tester les contrôleurs de Codeigniter?

Je voudrais faire du développement piloté par les tests et j'ai lu le tutoriel sur le site PHPUnits, mais encore une fois l'exemple semble si abstrait. La plupart de mes fonctions de codeigniter affichent des données.

Existe-t-il un livre ou un excellent tutoriel avec une application pratique et des exemples de tests PHPUnit?

58
zechdc

Il semble que vous compreniez la structure/syntaxe de base de la façon d'écrire des tests et des tests unitaires Le code CodeIgniter ne devrait pas être différent du test de code non CI, donc je veux me concentrer sur vos préoccupations/problèmes sous-jacents ...

J'ai eu des questions similaires il n'y a pas si longtemps avec PHPUnit. En tant que personne sans formation formelle, j'ai trouvé qu'au début, entrer dans la mentalité des tests unitaires semblait abstrait et contre nature. Je pense que la principale raison de cela - dans mon cas, et probablement aussi la vôtre de la question - est que vous ne vous êtes pas concentré sur [~ # ~] vraiment [~ # ~] travaille jusqu'à présent pour séparer les problèmes dans votre code.

Les affirmations de test semblent abstraites car la plupart de vos méthodes/fonctions effectuent probablement plusieurs tâches distinctes différentes. Une mentalité de test réussie nécessite un changement dans la façon dont vous pensez de votre code. Vous devez arrêter de définir le succès en termes de "ça marche?" Au lieu de cela, vous devriez vous demander: "cela fonctionne-t-il, fonctionnera-t-il bien avec un autre code, est-il conçu de manière à le rendre utile dans d'autres applications et puis-je vérifier qu'il fonctionne?"

Par exemple, ci-dessous est un exemple simplifié de la façon dont vous avez probablement écrit du code jusqu'à présent:

function parse_remote_page_txt($type = 'index')
{
  $remote_file = ConfigSingleton::$config_remote_site . "$type.php";
  $local_file  = ConfigSingleton::$config_save_path;

  if ($txt = file_get_contents($remote_file)) {
    if ($values_i_want_to_save = preg_match('//', $text)) {
      if (file_exists($local_file)) {
        $fh = fopen($local_file, 'w+');
        fwrite($fh, $values_i_want_to_save);
        fclose($fh);
        return TRUE;
      } else {
        return FALSE;
      }
  } else {
    return FALSE;
  }  
}

Exactement ce qui se passe ici n'est pas important. J'essaie d'illustrer pourquoi ce code est difficile à tester:

  • Il utilise une classe de configuration singleton pour générer des valeurs. Le succès de votre fonction dépend des valeurs du singleton, et comment pouvez-vous tester que cette fonction fonctionne correctement dans un isolement complet lorsque vous ne pouvez pas instancier de nouveaux objets de configuration avec des valeurs différentes? Une meilleure option pourrait être de passer votre fonction à $config argument composé d'un objet de configuration ou d'un tableau dont vous pouvez contrôler les valeurs. Ceci est généralement appelé " injection de dépendance " et il y a des discussions sur cette technique partout dans les interwebs.

  • Remarquez les instructions IF imbriquées. Le test signifie que vous recouvrez chaque ligne exécutable avec une sorte de test. Lorsque vous imbriquez des instructions IF, vous créez de nouvelles branches de code qui nécessitent un nouveau chemin de test.

  • Enfin, voyez-vous comment cette fonction, bien qu'elle semble faire une chose (analyser le contenu d'un fichier distant), effectue en fait plusieurs tâches? Si vous séparez avec zèle vos préoccupations, votre code devient infiniment plus testable. Une façon beaucoup plus testable de faire la même chose serait ...


class RemoteParser() {
  protected $local_path;
  protected $remote_path;
  protected $config;

  /**
   * Class constructor -- forces injection of $config object
   * @param ConfigObj $config
   */
  public function __construct(ConfigObj $config) {
    $this->config = $config;
  }

  /**
   * Setter for local_path property
   * @param string $filename
   */
  public function set_local_path($filename) {
    $file = filter_var($filename);
    $this->local_path = $this->config->local_path . "/$file.html";
  }

  /**
   * Setter for remote_path property
   * @param string $filename
   */
  public function set_remote_path($filename) {
    $file = filter_var($filename);
    $this->remote_path = $this->config->remote_site . "/$file.html";
  }

  /**
   * Retrieve the remote source
   * @return string Remote source text
   */
  public function get_remote_path_src() {
    if ( ! $this->remote_path) {
      throw new Exception("you didn't set the remote file yet!");
    }
    if ( ! $this->local_path) {
      throw new Exception("you didn't set the local file yet!");
    }
    if ( ! $remote_src = file_get_contents($this->remote_path)) {
      throw new Exception("we had a problem getting the remote file!");
    }

    return $remote_src;
  }

  /**
   * Parse a source string for the values we want
   * @param string $src
   * @return mixed Values array on success or bool(FALSE) on failure
   */
  public function parse_remote_src($src='') {
    $src = filter_validate($src);
    if (stristr($src, 'value_we_want_to_find')) {
      return array('val1', 'val2');
    } else {
      return FALSE;
    }
  }

  /**
   * Getter for remote file path property
   * @return string Remote path
   */
  public function get_remote_path() {
    return $this->remote_path;
  }

  /**
   * Getter for local file path property
   * @return string Local path
   */
  public function get_local_path() {
    return $this->local_path;
  }
}

Comme vous pouvez le voir, chacune de ces méthodes de classe gère une fonction particulière de la classe qui est facilement testable. La récupération de fichiers à distance a-t-elle fonctionné? Avons-nous trouvé les valeurs que nous essayions d'analyser? Etc. Tout d'un coup, ces affirmations abstraites semblent beaucoup plus utiles.

À mon humble avis, plus vous vous plongez dans les tests, plus vous vous rendez compte qu'il s'agit davantage d'une bonne conception de code et d'une architecture sensible que de simplement vous assurer que les choses fonctionnent comme prévu. Et c'est là que les avantages de OOP commencent vraiment à briller. Vous pouvez très bien tester le code procédural, mais pour un grand projet avec des tests de pièces interdépendants, il existe un moyen d'appliquer une bonne conception. Je sais que peut être un appât troll pour certaines personnes procédurales, mais bon.

Plus vous testez, plus vous vous retrouvez à écrire du code et à vous demander: "Serai-je capable de tester cela?" Et sinon, vous changerez probablement la structure ici et là.

Cependant, le code n'a pas besoin d'être élémentaire pour être testable. Stubbing and mocking vous permet de tester des opérations externes dont le succès ou l'échec est entièrement hors de contrôle. Vous pouvez créer fixtures pour tester les opérations de base de données et à peu près tout le reste.

Plus je teste, plus je me rends compte que si j'ai du mal à tester quelque chose, c'est probablement parce que j'ai un problème de conception sous-jacent. Si je redresse cela, il en résulte généralement toutes les barres vertes dans mes résultats de test.

Enfin, voici quelques liens qui m'ont vraiment aidé à commencer à réfléchir de manière conviviale aux tests. Le premier est ne liste ironique de ce qu'il ne faut PAS faire si vous voulez écrire du code testable . En fait, si vous parcourez l'ensemble de ce site, vous trouverez de nombreuses informations utiles qui vous aideront à vous mettre sur la voie d'une couverture de code à 100%. Un autre article utile est ceci discussion sur l'injection de dépendance .

Bonne chance!

95
rdlowrey

J'ai essayé en vain d'utiliser PHPUnit avec Codeigniter. Par exemple, si je voulais tester mes modèles CI, je suis tombé sur le problème de savoir comment obtenir une instance de ce modèle car il a en quelque sorte besoin de l'ensemble du framework CI pour le charger. Considérez par exemple comment vous chargez un modèle:

$this->load->model("domain_model");

Le problème est que si vous regardez la super classe pour une méthode de chargement, vous ne la trouverez pas. Ce n'est pas aussi simple si vous testez Plain Old PHP Objets où vous pouvez facilement simuler vos dépendances et tester les fonctionnalités.

Par conséquent, je me suis contenté de classe de tests unitaires de CI .

my apps grab a bunch of data from the database then display it in some sort of table.

Si vous testez vos contrôleurs, vous testez essentiellement la logique métier (si vous en avez) ainsi que la requête SQL qui "récupère un tas de données" de la base de données. Il s'agit déjà d'un test d'intégration.

Le meilleur moyen est de tester le modèle CI d'abord pour tester la capture des données --- cela sera utile si vous avez une requête très compliquée - puis le contrôleur à côté de tester la logique métier qui est appliquée aux données saisies par le modèle CI. C'est une bonne pratique de ne tester qu'une seule chose à la fois. Alors, que testerez-vous? La requête ou la logique métier?

Je suppose que vous voulez d'abord tester la saisie des données, les étapes générales sont

  1. Obtenez des données de test et configurez votre base de données, vos tables, etc.

  2. Avoir un mécanisme pour remplir la base de données avec des données de test ainsi que la supprimer après le test. l'extension de la base de données de PHPUnit a un moyen de le faire, même si je ne sais pas si cela est pris en charge par le cadre que vous avez publié. Faites le nous savoir.

  3. Écrivez votre test, passez-le.

Votre méthode de test pourrait ressembler à ceci:

// At this point database has already been populated
public function testGetSomethingFromDB() {
    $something_model = $this->load->model("domain_model");
    $results = $something_model->getSomethings();
    $this->assertEquals(array(
       "item1","item2"), $results);

}
// After test is run database is truncated. 

Juste au cas où vous voudriez utiliser la classe de tests unitaires de CI, voici un extrait de code modifié d'un test que j'ai écrit en l'utilisant:

class User extends CI_Controller {
    function __construct() {
        parent::__construct(false);
        $this->load->model("user_model");
        $this->load->library("unit_test");
    }

public function testGetZone() {
            // POPULATE DATA FIRST
    $user1 = array(
        'user_no' => 11,
        'first_name' => 'First',
        'last_name' => 'User'
    );

    $this->db->insert('user',$user1);

            // run method
    $all = $this->user_model->get_all_users();
            // and test
    echo $this->unit->run(count($all),1);

            // DELETE
    $this->db->delete('user',array('user_no' => 11));

}
2
Jeune