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 si grand! J'ai finalement eu PHPUnit travailler 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 génial si j'avais une fonction qui produirait une chaîne aussi prévisible. Habituellement, mes applications prennent un tas de données de la la base de données l'affiche ensuite dans une sorte de table. Alors, comment 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 codeigniter affichent des données.

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

56
demandé sur zechdc 2011-12-06 07:54:55

2 réponses

Il semble que vous compreniez la structure/syntaxe de base de la façon d'écrire des tests et le code unit testing CodeIgniter ne devrait pas être différent du code testing 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é que l'entrée dans l'État D'esprit des tests unitaires semblait abstraite et non naturelle au début. Je pense que la principale raison de cela-dans mon cas, et probablement le vôtre aussi de la question-Est-ce que vous ne vous êtes pas concentré sur vraiment travaillant pour séparer les préoccupations dans votre code jusqu'à présent.

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

Par exemple, voici 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;
  }  
}

Ce qui se passe ici n'a pas D'importance. 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 cela la fonction fonctionne correctement en isolation complète 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 un argument $config qui consiste en un objet de configuration ou un tableau dont vous pouvez contrôler les valeurs. Ceci est largement appelé "Dependency Injection " et il y a des discussions sur cette technique partout dans les interwebs.

  • Notez les instructions IF imbriquées. Test signifie que vous couvrez chaque ligne exécutable avec certains 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 plusieurs tâches? Si vous séparez avec zèle vos préoccupations, votre code devient infiniment plus testable. Beaucoup plus testable façon 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, chacun de ces méthodes de classe gère une fonction particulière de la classe qui est facilement testable. A la récupération de fichiers à distance de travail? 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 plongez dans les tests, plus vous réalisez qu'il s'agit plus d'une bonne conception de code et d'une architecture sensible que de simplement vous assurer que les choses fonctionnent comme prévu. Et voici où les avantages de la POO commencent vraiment à briller. Vous pouvez tester code de procédure très bien, mais pour un grand projet avec des pièces interdépendantes, les tests ont un moyen d'appliquer une bonne conception. Je sais que cela peut être un appât de troll pour certaines personnes procédurales, mais oh bien.

Plus vous testez, plus vous vous retrouverez à écrire du code et à vous demander: "vais-je pouvoir tester cela?"Et sinon, vous allez probablement changer la structure alors et là.

Cependant, le code n'a pas besoin d'être élémentaire pour être testées. Stubbing et moqueur vous permet de tester opérations extérieures dont le succès ou l'échec est totalement 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é à démarrer pensée dans un test-d'une façon amicale. Le premier est une liste tongue-in-cheek de ce QU'il ne faut pas faire si vous voulez écrire du code testable . En fait, si vous parcourez tout ce site, vous trouverez beaucoup de choses utiles qui vous aideront à vous mettre sur le chemin de la couverture de code 100%. Un autre article utile est ceci discussion de l'injection de dépendance .

Bonne chance!

93
répondu rdlowrey 2013-08-01 16:43:14

J'ai essayé sans succès D'utiliser PHPUnit avec Codeigniter. Par exemple, si je voulais tester mes modèles CI, j'ai rencontré le problème de la façon dont je vais obtenir une instance de ce modèle car il a besoin de tout le framework CI pour le charger. Considérez comment vous chargez un modèle par exemple:

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

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

Par conséquent, je me suis installé pour la classe de test unitaire 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 l'avez) ainsi que la requête sql qui "saisit un tas de données" de la base de données. C'est déjà le test d'intégration.

Le meilleur moyen est de tester le modèle CI d'abord pour tester la saisie 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 pour tester une seule chose à la fois. Alors, qu'allez-vous tester? 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 comme bien que le supprimer après le test. l'extension de base de données de PHPUnit a un moyen de le faire bien que je ne sais pas si cela est pris en charge par le framework que vous avez posté. Laissez-nous savoir.

  3. Ecrivez 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 voulez utiliser la classe de test unitaire 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
répondu Jeune 2011-12-06 04:59:08