BDD: from novice to ninja

Finalmente ho trovato il tempo per definire nel modo che volevo i test del software web che costruisco.

Questo post lo scrivo come promemoria per me e per chi vuole addentrarsi in questo mondo.

Le idee che stanno alla base di questa metodologia sono:

  • scrivere dei test da far girare prima di ogni deploy del software web
  • voglio scrivere poco codice php per i test
  • i test devono essere comprensibili al cliente (o meglio ai business analysts)
  • i test devono testare l’effettivo risultato sul browser
  • i test devono darmi delle chiare indicazioni di dove falliscono
  • devono funzionare nel mio ambiente di sviluppo cioè symfony 2.1 + vagrant (per i dettagli vedi Impostare il mio ambiente di sviluppo perfetto in OSX 10.8 Mountain Lion)

L’idea: behavior-driven development (BDD)

Nell’ambito dell’ingegneria del software, il behavior-driven development (abbreviato in BDD e traducibile in Sviluppo guidato dal comportamento) è una metodologia di sviluppo del software basata sul test-driven development (TDD)[1][2] Il BDD combina le tecniche generali e i principi del TDD con idee prese dal domain-driven design e dal desing e all’analisi orientato agli oggetti per fornire agli sviluppatore software e ai Business analysts degli strumenti e un processo condivisi per collaborare nello sviluppo software.[1][3]

Per quanto BDD sia principalmente un’idea di come lo sviluppo del software dovrebbe essere gestito sia da interessi di business e analisi tecniche, la pratica della BDD assume l’utilizzo di strumenti software specializzati per supportare il processo di sviluppo.[2] Sebbene questi strumenti siano spesso sviluppati in particolare per essere utilizzati in progetti BDD, possono essere visti anche come delle forme specializzate degli strumenti che supportano la TDD. Gli strumenti servono per aggiungere automazione all’ubiquitous language che è il tema centrale della BDD.

Se votete approfondire c’è un semplice articolo di Dan North dal titolo Introducing BDD di cui trovate la traduzione in italiano qui.

Ecco un test scritto con BDD:

Feature: Login
  In order to login
  As a portal user
  I need to be able to validate the username and password against portal

  Scenario: Link to login page
    Given I am on the homepage
    When I go to "Accedi"        
    Then I am on "login"

@javascript
  Scenario: Login as an existing user
    Given I am on "login"
    When I fill in "username" with "userU"
     And I fill in "password" with "password"
     And I press "Entra nel portale"
    Then I should see "Benvenuto firstU (utente)"

@javascript
  Scenario: Login as an existing powered user
    Given I am on "login"
    When I fill in "username" with "userPowered"
    And I fill in "password" with "password"
    And I press "Entra nel portale"
    Then I should see "Benvenuto UtenteP (user potenziato)"

@javascript
  Scenario: Login as an existing user admin
    Given I am on "login"
    When I fill in "username" with "admin"
    And I fill in "password" with "password"
    And I press "Entra nel portale"
    Then I should see "Benvenuto UtenteA (amministratore)"

@javascript
  Scenario: Login as an unexisting user
    Given I am on "login"
    When I fill in "username" with "adminasdasd"
    And I fill in "password" with "moodlesdfasdf"
    And I press "Entra nel portale"
    Then I should see "Bad credentials"

Il linguaggio utilizzato è il Gherkin e come vedete ricalca molto da vicino quelle che sono le user stories.

Su come e cosa scrivere in questo test vi segnalo due risorse:

 I collanti: Behat, Mink e Selenium2

Le tecnologie che verranno usate per raggiungere l’obiettivo sono:

  • Behat: è un framework behavior-driven development open source per PHP 5.3 e 5.4. Vedi: http://docs.behat.org/
  • Mink: per verificare che l’applicazione web si comporti correttamente abbiamo bisogno di un modo per simulare l’interazione tra applicazione e browser, Mink è un framework open source per acceptance test scritto in PHP 5.3. Vedi: http://mink.behat.org/
  • Selenium2: simile a Mink ma con la possibilità di “comandare” i browser effettivamente presenti nel nostro ambiente. Useremo la versione selenium2 server che ci permetterà di testare anche pagine con javascript. Vedi http://docs.seleniumhq.org/

 Un esempio pratico: fos_user_bundle

Come esempio e promemoria testiamo il bundle fos_user_bundle che permette l’autenticazione degli utenti (e non solo) in un progetto symfony2.1.

Installazione dei collanti.

Per installare i Behat, Mink e tutti gli strumenti necessari basta modificare il file composer.json aggiungendo le seguenti righe:

"config": {
     "bin-dir": "bin/"
 },
 "require": {
[...........]
    "behat/behat": "2.4.*@stable",
    "behat/mink": "1.4.*@stable",
    "behat/symfony2-extension": "*",
    "behat/mink-extension": "*",
    "behat/mink-browserkit-driver": "*",
    "behat/mink-selenium2-driver": "*",
    "behat/mink-goutte-driver": "*",
    "behat/mink-sahi-driver": "*",
    "phpunit/phpunit": "3.7.*"
 },

Poi digitare il solito:

composer update

che si preoccuperà di ottenere i vari pacchetti e le relative dipendenze. Verrà creata anche la cartella bin con l’eseguibile behat.

Salviamo in una cartella anche l’ultima version disponibile del file .jar di selenium 2 server (ad esempio http://selenium.googlecode.com/files/selenium-server-standalone-2.33.0.jar)

Ora abbiamo tutti i nostri strumenti.

Configurazione e inizializzazione

Configuriamo Behat creando il file app/config/behat.yml con il seguente contenuto:

default:
  formatter:
    name: progress
  extensions:
    Behat\Symfony2Extension\Extension:
      mink_driver: true
      kernel:
        env: test
        debug: false
    Behat\MinkExtension\Extension:
      base_url: 'http://localhost:8080/'
      javascript_session:  'selenium2'
      browser_name: firefox
      goutte:              ~
      selenium2:

Si noti l’integrazione tra le diverse componenti e l’utilizzo del server web sulla porta 8080 che vagrant ha messo a disposizione.

A questo punto possiamo procedere all’inizializzazione dell’ambiente di test con il comando:

./bin/behat --init --config=app/config/behat.yml @ZenPortalBundle

Questo comando crea la struttura delle cartelle nel vostro progetto, il tutto contenuto nel folder Features. Questa sarà la cartella di riferimento per i nostri test.

Creazione di un test

Per creare un test BDD basta scrivere un file .feature nel linguaggio Gherkin e posizionarlo nella cartella Features.

Ecco quello che useremo (file login.feature):

Feature: Login
  In order to login
  As a portal user
  I need to be able to validate the username and password against portal

  Scenario: Link to login page
    Given I am on the homepage
    When I go to "Accedi"        
    Then I am on "login"

@javascript
  Scenario: Login as an existing user
    Given I am on "login"
    When I fill in "username" with "userU"
     And I fill in "password" with "password"
     And I press "Entra nel portale"
    Then I should see "Benvenuto firstU (utente)"

@javascript
  Scenario: Login as an existing powered user
    Given I am on "login"
    When I fill in "username" with "userPowered"
    And I fill in "password" with "password"
    And I press "Entra nel portale"
    Then I should see "Benvenuto UtenteP (user potenziato)"

@javascript
  Scenario: Login as an existing user admin
    Given I am on "login"
    When I fill in "username" with "admin"
    And I fill in "password" with "password"
    And I press "Entra nel portale"
    Then I should see "Benvenuto UtenteA (amministratore)"

@javascript
  Scenario: Login as an unexisting user
    Given I am on "login"
    When I fill in "username" with "adminasdasd"
    And I fill in "password" with "moodlesdfasdf"
    And I press "Entra nel portale"
    Then I should see "Bad credentials"

Integrazione con Mink e selenium2

Ora si tratta di sistemare il file FeatureContext.php per integrarlo con Mink. Visto che mettiamo mano a questo file ne approfittiamo anche per alcune aggiunte utili. Aggiungiamo una porzione di codice che nel caso in cui un test fallisce mi crea nella cartella build una immagine con lo screenshot del browser nel punto in cui il test fallisce e con un nome significativo (lo scenario).

<?php
namespace Zen\PortalBundle\Features\Context;
use Symfony\Component\HttpKernel\KernelInterface;
use Behat\Symfony2Extension\Context\KernelAwareInterface;
use Behat\MinkExtension\Context\MinkContext;
use Behat\Mink\Exception\ElementNotFoundException;
use Behat\Mink\Exception\UnsupportedDriverActionException;
use Behat\Mink\Driver\Selenium2Driver;
//
// Require 3rd-party libraries here:
//
//require_once 'PHPUnit/Autoload.php';
//require_once 'PHPUnit/Framework/Assert/Functions.php';
//
/**
 * Feature context.
 */
class FeatureContext extends MinkContext implements KernelAwareInterface
{
 private $kernel;
 private $parameters;
/**
 * Initializes context with parameters from behat.yml.
 *
 * @param array $parameters
 */
 public function __construct(array $parameters)
 {
 $this->parameters = $parameters;
 }
/**
 * Sets HttpKernel instance.
 * This method will be automatically called by Symfony2Extension ContextInitializer.
 *
 * @param KernelInterface $kernel
 */
 public function setKernel(KernelInterface $kernel)
 {
 $this->kernel = $kernel;
 }

/**
 * Take screenshot when step fails.
 * Works only with Selenium2Driver.
 *
 * @AfterStep
 */
 public function takeScreenshotAfterFailedStep($event)
 {
 if (4 === $event->getResult()) {
 $driver = $this->getSession()->getDriver();
 if (!($driver instanceof Selenium2Driver)) {
 // throw new UnsupportedDriverActionException('Taking screenshots is not supported by %s, use Selenium2Driver instead.', $driver);
 return;
 }
 $directory = 'build/behat/'.$event->getLogicalParent()->getFeature()->getTitle().'.'.$event->getLogicalParent()->getTitle();
 if (!is_dir($directory)) {
 mkdir($directory, 0777, true);
 }
 $filename = sprintf('%s_%s_%s.%s', $this->getMinkParameter('browser_name'), date('c'), uniqid('', true), 'png');
 file_put_contents($directory.'/'.$filename, $driver->getScreenshot());
 }
 }
public function assertPageContainsText($text)
 {
 $this->getSession()->wait(10000, '(typeof(jQuery)=="undefined" || (0 === jQuery.active && 0 === jQuery(\':animated\').length))');
 parent::assertPageContainsText($text);
 }

 }

 

Esecuzione del test

Ora siamo pronti ad eseguire il test.

Prima di tutto facciamo partire il server selenium con il comando:

java -jar selenium-server-standalone-2.33.0.jar

Poi facciamo partire i nostri test con:

./bin/behat --config=app/config/behat.yml @ZenPortalBundle

Se tutto è andato a buon fine vedremo avanzare i test attraverso i browser e il server selenium e otteniamo un bel verde alla fine.

 

 

Fonti

Alcune risorse che ho trovato utili in questo studio:

In modo particolare segnalo l’intervento di Tuin al phpDay 2013  (http://2013.phpday.it/talk/automated-acceptance-testing-with-behat-and-mink/) di cui qui trovare le slide.

 

 

 

 

 

ARTICOLI
sviluppo web