Skip to content
Snippets Groups Projects
start_in_bokeh.fr.md 13.37 KiB

Créer un nouvel écran Front dans Bokeh

Toute fonctionnalité Bokeh est développée en suivant la méthodologie Test Driven Development.

Ce qui veut dire qu'il faut toujours commencer par écrire un test.

L'équipe Bokeh privilégie actuellement l'approche dite du scénario qui consiste à créer un répertoire sous tests/scenarios/ qui contiendra tous les tests de la nouvelle fonctionnalité.

Le premier test à écrire concernera l'accès à l'url qui affichera le nouvel écran.

Bokeh utilisant l'architecture MVC de ZendFramework (library/storm/zf/documentation/manual/core/en/index.html), il s'agira du test d'une action d'un controller.

Bokeh fourni un objet de base pour les tests de controller AbstractControllerTestCase dans tests/application/modules/AbstractControllerTestCase.php

Par exemple, partons du principe que nous souhaitons fournir la possibilité de gérer des listes de choses à faire.

Nous commençons par créer un répertoire tests/scenarios/TodoList et dans ce répertoire un fichier TodoListTest.php

<?php
class TodoListTest extends AbstractControllerTestCase {
  /** @test */
  public function pageShouldContainsH1ChosesAFaire() {
  }
}

Remarquez que la fonction de test a la forme "contextShouldExpectation", vous trouverez cette forme très souvent dans les tests de Bokeh.

Pour lancer ce test en particulier en ligne de commande, placer vous dans le répertoire de votre Bokeh puis exécutez:

phpunit -c tests/phpunit.xml --filter pageShouldContainsH1ChosesAFaire TodoListTest tests/scenarios/TodoList/TodoListTest.php

Vous devriez voir:

OK (1 test, 0 assertions)

Nous pouvons simuler l'accès à une url avec la fonction AbstractControllerTestCase::dispatch($url = null, $throw_exceptions = true, $headers = [])

<?php
class TodoListTest extends AbstractControllerTestCase {
  /** @test */
  public function pageShouldContainsH1ChosesAFaire() {
    $this->dispatch('/todo');
  }
}

Avec ce dispatch nous venons de décider que l'url d'accès de la fonctionnalité sera /todo.

Si vous relancez les tests vous devriez voir:

There was 1 error:

1) TodoListTest::pageShouldContainsH1ChosesAFaire
Zend_Controller_Dispatcher_Exception: Invalid controller specified (todo)

library/storm/zf/library/Zend/Controller/Dispatcher/Standard.php:240
library/ZendAfi/Controller/Dispatcher/Standard.php:29
library/storm/zf/library/Zend/Controller/Front.php:934
tests/application/modules/AbstractControllerTestCase.php:295
tests/scenarios/TodoList/TodoListTest.php:5

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

ZendFramework nous informe que nous lui avons demandé un controller qui n'existe pas (todo), nous allons donc le créér.

Bokeh suit une architecture modulaire dans laquelle:

  • application/modules contient des modules (opac, admin, ...)
  • application/modules/[nom du module]/controllers contient des fichier XxxxxxxController.php (AbonneController.php, RechercheController.php, ...)
  • chaque fichier controller contient au moins une fonction xxxxAction() (pretsAction, reservationsAction, ...)

Bokeh fournit un objet de base pour tous les controllers, ZendAfi_Controller_Action.

Créons un fichier application/modules/opac/controllers/TodoController.php:

<?php
class TodoController extends ZendAfi_Controller_Action {
}

Si vous relancez les tests vous devriez avoir:

There was 1 error:

1) TodoListTest::pageShouldContainsH1ChosesAFaire
Error: Call to a member function index() on null

library/ZendAfi/Controller/Action.php:153
library/storm/zf/library/Zend/Controller/Action.php:494
library/storm/zf/library/Zend/Controller/Dispatcher/Standard.php:284
library/ZendAfi/Controller/Dispatcher/Standard.php:29
library/storm/zf/library/Zend/Controller/Front.php:934
tests/application/modules/AbstractControllerTestCase.php:295
tests/scenarios/TodoList/TodoListTest.php:5

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

ZendFramework nous informe que nous lui avons demandé une action qui n'existe pas (index).

Bokeh utilise la notion de routage des urls avec pour motif [module]/[controller]/[action]. Par exemple opac/recherche/simple équivaut à module opac, controller RechercheController.php, action simpleAction(). Chacune des parties de l'url est optionelle:

  • lorsque l'action n'est pas donnée on considère qu'il s'agit de indexAction()
  • lorsque le controller n'est pas donné on considère qu'il s'agit de IndexController.php
  • lorsque le module n'est pas donné on considère qu'il s'agit d'opac

Autrement dit l'url par défaut / correspond à /opac/index/index.

Créons une action indexAction()

<?php
class TodoController extends ZendAfi_Controller_Action {
  public function indexAction() {
  }
}

Si vous relancez les tests vous devriez avoir:

There was 1 error:

1) TodoListTest::pageShouldContainsH1ChosesAFaire
Zend_View_Exception: script 'todo/index.phtml' not found in path (application/modules/opac/views/scripts/)

library/storm/zf/library/Zend/View/Abstract.php:875
library/storm/zf/library/Zend/View/Abstract.php:783
library/templates/Historic/Template.php:40
library/ZendAfi/View/Helper/Layout/Opac.php:26
library/storm/zf/library/Zend/View/Abstract.php:317
application/modules/opac/views/scripts/module.phtml:2
library/storm/zf/library/Zend/View.php:107
library/storm/zf/library/Zend/View/Abstract.php:787
library/ZendAfi/Controller/Action/Helper/ViewRenderer.php:131
library/storm/zf/library/Zend/Controller/Action/Helper/ViewRenderer.php:923
library/storm/zf/library/Zend/Controller/Action/Helper/ViewRenderer.php:962
library/storm/zf/library/Zend/Controller/Action/HelperBroker.php:166
library/storm/zf/library/Zend/Controller/Action.php:504
library/storm/zf/library/Zend/Controller/Dispatcher/Standard.php:284
library/ZendAfi/Controller/Dispatcher/Standard.php:29
library/storm/zf/library/Zend/Controller/Front.php:934
tests/application/modules/AbstractControllerTestCase.php:295
tests/scenarios/TodoList/TodoListTest.php:5

ERRORS!
Tests: 1, Assertions: 0, Errors: 1.

ZendFramework nous informe que le rendu de cette action n'existe pas (todo/index.phtml).

Bokeh utilise une fonction du ZendFramework qui effectue automatiquement le rendu de l'action courante en cherchant un fichier dans le répertoire views/scripts/ du module courant, dans un sous-répertoire du nom du controller courant (todo) ayant comme nom de fichier le nom de l'action courante plus l'extension .phtml.

Dans notre cas, cela correspond à views/scripts/todo/index.phtml

Créons ce fichier vide.

touch application/modules/opac/views/scripts/todo/index.phtml

Si vous relancez les tests vous devriez avoir:

OK (1 test, 0 assertions)

L'accès à l'url s'est bien passé, l'action a été exécutée et rendue en html, mais notre test n'a rien vérifié pour l'instant (0 assertions).

Ajoutons une vérification:

<?php
class TodoListIndexTest extends AbstractControllerTestCase {
  /** @test */
  public function pageShouldContainsH1ChosesAFaire() {
    $this->dispatch('/todo');
    $this->assertContains('<h1>Choses à faire</h1>', $this->_response->getBody());
  }
}

Si vous relancez les tests vous devriez avoir:

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

Le test nous informe qu'il n'y a aucune balise h1 contenant "Choses à faire" dans le rendu de l'action.

Ajoutons simplement cette balise dans le fichier todo/index.phtml

<h1>Choses à faire</h1>

Si vous relancez les tests vous devriez avoir:

OK (1 test, 1 assertion)

Nous allons maintenant écrire un 2ème test pour vérifier la présence d'un input

  /** @test */
  public function pageShouldContainsInputText() {
    $this->dispatch('/todo');
    $this->assertXPath('//input[@type="text"]');
  }

Si vous lancez ce test vous devriez avoir une Failure

There was 1 failure:

FAILURES!
Tests: 1, Assertions: 0, Failures: 1.

Le test nous informes qu'il n'y a pas de balise input dans le phtml, on va donc le rajouter

<h1>Choses à faire</h1>
<input type="text"></input>

Et relancer le test

OK (1 test, 0 assertions)

Avant de passer au test suivant, une bonne pratique de développement est de passer par une phase de refactoring quand il y a du code redondant, ici dans nos test celà concerne le dispatch qui est répété 2 fois, on va donc le remonter dans un setUp

  public function setUp() {
    parent::setUp();
    $this->dispatch('/todo');
  }


  /** @test */
  public function pageShouldContainsChosesAFaire() {
    $this->assertContains('<h1>Choses à faire</h1>', $this->_response->getBody());
  }


  /** @test */
  public function pageShouldContainsInputText() {
    $this->assertXPath('//input[@type="text"]');
  }

Et on vérifie toujours, après la moindre modification que nos tests sont toujours green

OK (2 tests, 1 assertion)

Test suivant on rajoute l'existence du bouton Ajouter

  /** @test */
  public function pageShouldContainsBoutonAjouter() {
    $this->assertXPathContentContains('//button', 'Ajouter');
  }

En lancant le test, il nous informe que le boutton est manquant

FAILURES!
Tests: 1, Assertions: 0, Failures: 1.

On va donc le rajouter dans le index.phtml

<h1>Choses à faire</h1>
<input type="text"></input><button>Ajouter</button>

Et relancer le test

OK (1 test, 0 assertions)

On va maintenant avoir besoin d'un formulaire pour le submit du boutton, on commence à écrire le test

  /** @test */
  public function pageShouldContainsPostForm() {
    $this->assertXPath('//form[@method="post"][@action="/todo/add"]');
  }

En lancant le test, il nous avertit que ce formulaire n'existe pas

FAILURES!
Tests: 1, Assertions: 0, Failures: 1.

On rajoute dans le index, le form

<h1>Choses à faire</h1>
<form method="post" action="/todo/add">
  <input type="text"></input>
  <button>Ajouter</button>
</form>

Et on relance le test pour le green

OK (1 test, 0 assertions)

On va ajouter maintenant le tableau des tâches à faire avec la première valeur Se lever

  /** @test */
  public function pageShouldContainsTableWithManger() {
    $this->assertXPathContentContains('//table//tr//td', 'Se lever');
  }

Qui va nous souslever une Failure

FAILURES!
Tests: 1, Assertions: 0, Failures: 1.

On la corrige en mettant le contenu dans le html

<table>
  <tr>
    <td>Se lever</td>
  </tr>
</table>

Et valider le test

OK (1 test, 0 assertions)

On va tout de suite commencer à rajouter le bouton Suprimer, en commençant par son test

  /** @test */
  public function pageShouldContainsButtonSupprimer() {
    $this->assertXPathContentContains('//table//tr//td//button', 'Supprimer');
  }

Qui va avoir une Failure

FAILURES!
Tests: 1, Assertions: 0, Failures: 1.

Et faire l'ajout du coté html, pour avoir le green

<td><button>Supprimer</button></td>

A partir de la on va commencer a s'occuper du model et créé l'objet On va donc ajouter une fixture sur nos test dans le setup

  public function setUp() {
    parent::setUp();
    Class_Todo::newInstance();
    $this->dispatch('/todo');
  }

La on va avoir des Erreurs, la classe n'existant pas

Error: Class 'Class_Todo' not found

On va donc la créé, nous allons donc créé un model storm. Voir cette doc sur Gitlab: https://git.afi-sa.net/afi/storm

class Class_Todo extends Storm_Model_Abstract {
  protected $_table_todo = 'todo_list';
}

Il faudra aussi rajouter ceci en entête de la class test

class TodoListIndexTest extends AbstractControllerTestCase {
  protected $_storm_default_to_volatile = true;

Les tests vont donc revenir au vert On va donc passer à la prochaine étape qui consiste à vouloir obtenir le contenu de la fixture dans le html

<td><?php echo (Class_Todo::find(1))->getLabel(); ?></td>

Notre test sur l'existance de "Se lever" ne fonctionne donc plus, il y a une erreur On va rajouter le label dans la fixture

    Class_Todo::newInstance(['id' => 1, 'label' => 'Se lever'])
      ->assertSave();

Ici on va faire une petite parenthèse, nos tests fonctionnent mais le visuel réel lui non, car notre nouveau objet n'existe en BD. On va donc s'occuper de la partie BD, créé notre tables et les champs nécessaires. On commence bien sur par un test, tous les tests de BD se trouvent au meme endroit: /php/bokeh/tests/db/UpgradeDBTest.php

class UpgradeDB_395_Test extends UpgradeDBTestCase {
  public function prepare() {
    try {
      $this->query('drop table todo_list');
    }  catch(Exception $e) {}
  }


  public function datas() {
    return
      [
       ['id',  'int(11) unsigned'],
       ['label', 'varchar(255)']
      ];
  }


  /**
   * @test
   * @dataProvider datas
   */
  public function fieldShouldBe($field, $type) {
    $this->assertFieldType('todo_list', $field, $type);
  }
}

Et donc le patch correspondant, pour corrigé l'erreur du test L'éxecution du test, va lancer le patch et donc créé automatiquement la table en BD.

<?php
try {
  $adapter = Zend_Db_Table_Abstract::getDefaultAdapter();

  $adapter->query('CREATE TABLE if not exists `todo_list` ( '
                  . 'id int(11) unsigned not null auto_increment,'
                  . 'label varchar(255) not null,'
                  . 'primary key (id)'
                  . ') engine=MyISAM default charset=utf8');

} catch(Exception $e) {}