Automatizando test funcionales con Behat y Drupal: Instalación y puesta en marcha desde cero

¿Qué es Behat?

Behat es una herramienta de BDD (Behaviour Driven Development) que se utiliza para comprobar el comportamiento de una aplicación desde el punto de vista de un final. Es muy popular el uso de esta herramienta para pruebas de automatización de casos, utilizando escenarios legibles para los humanos.

Para escribir los test se usa el lenguaje Gherkin, muy similar al Inglés, de forma que se puedan escribir los test de la forma "Teniendo en cuenta que... Entonces debería...". Se puede además extender escribiendo funciones PHP personalizadas en el archivo FeatureContest.php que se crea dentro de la carpeta bootstrap.

¿Cuando usar Behat?

Behat ayuda a cumplir con las especificaciones y requisitos del cliente porque funciona con test que describen escenarios de posibles comportamientos del usuario en la web. Estos test pueden ser creados y mantenidos por cualquier persona, ya sea un gerente de proyecto, un desarrollador o cualquier otra parte interesada en el proyecto.

Los test automatizados de Behat pueden ayudar a:

  • Comprobar datos y contenido estático en una web.
  • Comprobar acciones sobre botones, enlaces y campos.
  • Comprobar formularios.
  • Comprobar Flujos de trabajo como registros o procesos de compra.
  • Comprobar que no haya regresiones en el código.

¿Donde no puede ayudar Behat?

  • Comprobar datos dinámicos.
  • Procesos sobre imágenes.
  • Códigos de respuesta de enlaces de un sitio web.

 

A continuación pasamos a la fase de instalación, para poner en marcha Behat en nuestro Drupal desde cero.

Instalación

Se puede instalar de forma cómoda y sencilla mediante composer. Agrega estas lineas a tu composer.json en Drupal, o bien, en una carpeta /behat aparte.

        "require": {
                "drupal/drupal-extension": "~3.0",
                "guzzlehttp/guzzle" : "^6.0@dev"
        } ,
        "config": {
                "bin-dir": "bin/"
        },
        "require-dev": {
                "behat/behat": "^3.4",
                "behat/mink": "^1.7",
                "behat/mink-extension": "^2.3",
                "behat/mink-browserkit-driver": "^1.3"
        }

Luego dejamos que composer haga su trabajo:

$ composer install

Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 35 installs, 0 updates, 0 removals
  - Installing symfony/event-dispatcher (v3.4.12): Downloading (100%)         
  - Installing psr/container (1.0.0): Downloading (100%)         
  - Installing symfony/dependency-injection (v3.4.12): Downloading (100%)         
  - Installing drupal/core-utility (8.5.5): Downloading (100%)         
  - Installing drupal/core-render (8.5.5): Downloading (100%)         
  - Installing paragonie/random_compat (v2.0.17): Downloading (100%)         
  - Installing symfony/process (v3.4.12): Downloading (100%)         
  - Installing drupal/drupal-driver (v1.4.0): Loading from cache
  - Installing instaclick/php-webdriver (1.4.5): Loading from cache
  - Installing symfony/css-selector (v3.4.12): Downloading (100%)         
  - Installing behat/mink (v1.7.1): Downloading (100%)         
  - Installing behat/mink-selenium2-driver (v1.3.1): Loading from cache
  - Installing psr/http-message (1.0.1): Downloading (100%)         
  - Installing guzzlehttp/psr7 (1.4.2): Downloading (100%)         
  - Installing guzzlehttp/promises (v1.3.1): Downloading (100%)         
  - Installing guzzlehttp/guzzle (dev-master 7bc46be): Cloning 7bc46be28e from cache
  - Installing symfony/polyfill-mbstring (v1.8.0): Downloading (100%)         
  - Installing symfony/polyfill-ctype (v1.8.0): Downloading (100%)         
  - Installing symfony/dom-crawler (v4.1.1): Downloading (100%)         
  - Installing symfony/browser-kit (v4.1.1): Downloading (100%)         
  - Installing fabpot/goutte (v3.2.3): Downloading (100%)         
  - Installing behat/mink-browserkit-driver (1.3.3): Downloading (100%)         
  - Installing behat/mink-goutte-driver (v1.2.1): Downloading (100%)         
  - Installing symfony/filesystem (v4.1.1): Downloading (100%)         
  - Installing symfony/config (v4.1.1): Downloading (100%)         
  - Installing symfony/yaml (v4.1.1): Downloading (100%)         
  - Installing symfony/translation (v4.1.1): Downloading (100%)         
  - Installing symfony/console (v4.1.1): Downloading (100%)         
  - Installing symfony/class-loader (v3.4.12): Downloading (100%)         
  - Installing container-interop/container-interop (1.2.0): Downloading (100%)         
  - Installing behat/transliterator (v1.2.0): Loading from cache
  - Installing behat/gherkin (v4.5.1): Loading from cache
  - Installing behat/behat (v3.4.3): Loading from cache
  - Installing behat/mink-extension (2.3.1): Loading from cache
  - Installing drupal/drupal-extension (v3.4.1): Loading from cache
symfony/event-dispatcher suggests installing symfony/http-kernel ()
symfony/dependency-injection suggests installing symfony/expression-language (For using expressions in service container configuration)
symfony/dependency-injection suggests installing symfony/finder (For using double-star glob patterns or when GLOB_BRACE portability is required)
symfony/dependency-injection suggests installing symfony/proxy-manager-bridge (Generate service proxies to lazy load them)
paragonie/random_compat suggests installing ext-libsodium (Provides a modern crypto API that can be used to generate random bytes.)
behat/mink suggests installing behat/mink-zombie-driver (fast and JS-enabled headless driver for any app (requires node.js))
guzzlehttp/guzzle suggests installing psr/log (Required for using the Log middleware)
symfony/translation suggests installing psr/log-implementation (To use logging capability in translator)
symfony/console suggests installing psr/log-implementation (For using the console logger)
symfony/console suggests installing symfony/lock ()
symfony/class-loader suggests installing symfony/polyfill-apcu (For using ApcClassLoader on HHVM)
behat/behat suggests installing behat/symfony2-extension (for integration with Symfony2 web framework)
behat/behat suggests installing behat/yii-extension (for integration with Yii web framework)
Writing lock file
Generating autoload files

Después de esto, tendremos nuevas carpetas como:

/bin (Donde está el ejecutable de behat y otros como drush)

/vendor (todas las dependencias necesarias)

Ahora necesitamos este otro archivo:

behat.yml

default:
  suites:
    default:
      contexts:
        - FeatureContext
        - Drupal\DrupalExtension\Context\DrupalContext
        - Drupal\DrupalExtension\Context\MinkContext
  extensions:
    Behat\MinkExtension:
      goutte: ~
      selenium2: ~
      base_url: http://sitioatestear.com
    Drupal\DrupalExtension:
      blackbox: ~
      api_driver: 'drupal' 
      drush:
        alias: 'local'

Recuerda modificar la url que quieres testear en: base_url.

Luego:

$ bin/behat --init

+d features - place your *.feature files here
+d features/bootstrap - place your context classes here
+f features/bootstrap/FeatureContext.php - place your definitions, transformations and hooks here

Y el paso final:

$ bin/behat -dl

default | Dados (que )soy un usuario anónimo
default | Dados (que )no estoy conectado
default | Dados (que )estoy conectado como usuario con rol(es) :role
default | Dados I am logged in as a/an :role
default | Dados I am logged in as a user with the :role role(s) and I have the following fields:
default | Dados (que )estoy conectado como :name
default | Dados (que )estoy conectado com un usuario con permiso(s) :permissions
default | Entonces I should see (the text ):text in the :rowText row
default | Entonces I should not see (the text ):text in the :rowText row
default | Dados hago click en el enlace :link de la fila :rowText
default | Entonces debo ver el enlace :link de la fila :rowText
default | Dados la cache ha sido limpiada
default | Dados lanzo el cron
default | Dados (que )estoy viendo un contenido de tipo :type con el título :title
default | Dados un contenido de tipo :type con el título :title
default | Dados (que )estoy viendo mi contenido de tipo :type con el título :title
default | Dados :type con contenido:
default | Dados (que )veo un(a) :type( con contenido):
default | Entonces debo poder editar un contenido de tipo :type
default | Dados (que )estoy viendo un término de :vocabulary con el nombre :name
default | Dados un término de :vocabulary con el nombre :name
default | Dados users:
default | Dados :vocabulary términos:
default | Dados the/these (following )languages are available:
default | Entonces break
default | Dados (que )estoy en :path
default | Cuando visito :path
default | Cuando hago click en :link
default | Dados para el campo :field introduzco( el valor) :value
default | Dados introduzco( el valor) :value para el campo :field
default | Dados espero a que AJAX termine
default | Cuando /^presiono "(?P<button>(?:[^"]|\\")*)"$/
default | Cuando pulso el botón :button
default | Dados pulso la tecla :char en el campo :field
default | Entonces debo ver el enlace :link
default | Entonces no debo ver el enlace :link
default | Entonces I should not visibly see the link :link
default | Entonces debo ver el encabezado :heading
default | Entonces no debo ver el encabezado :heading
default | Entonces I (should ) see the button :button
default | Entonces I (should ) see the :button button
default | Entonces I should not see the button :button
default | Entonces I should not see the :button button
default | Cuando hago click en :link de( la zona) :region
default | Dados pulso( el botón) :button en( la zona) :region
default | Dados relleno con :value el campo :field en( la zona) :region
default | Dados relleno el campo :field con :value en( la zona) :region
default | Entonces debo ver el encabezado :heading en( la zona) :region
default | Entonces debo ver :heading como encabezado en( la zona) :region
default | Entonces debo ver el enlace :link en( la zona) :region
default | Entonces no debo ver el enlace :link en( la zona) :region
default | Entonces debo ver( el texto) :text en( la zona) :region
default | Entonces no debo ver( el texto) :text en( la zona) :region
default | Entonces debo ver el texto :text
default | Entonces no debo ver el texto :text
default | Entonces debo obtener una respuesta HTTP( con) código :code
default | Entonces no debo obtener una respuesta HTTP( con) código :code
default | Dados marco la opción :checkbox
default | Dados desmarco la opción :checkbox
default | Cuando selecciono el botón de radio :label con el id :id
default | Cuando selecciono el botón de radio :label
default | Dados /^estoy en la página de inicio/
default | Cuando /^voy a la página de inicio/
default | Dados /^estoy en "(?P<page>[^"]+)"$/
default | Cuando /^voy a "(?P<page>[^"]+)"$/
default | Cuando /^recargo la página$/
default | Cuando /^voy hacia atrás una página$/
default | Cuando /^voy hacia adelante una página$/
default | Cuando /^sigo "(?P<link>(?:[^"]|\\")*)"$/
default | Cuando /^relleno "(?P<field>(?:[^"]|\\")*)" con "(?P<value>(?:[^"]|\\")*)"$/
default | Cuando /^(?:|I )fill in "(?P<field>(?:[^"]|\\")*)" with:$/
default | Cuando /^relleno con "(?P<value>(?:[^"]|\\")*)" a "(?P<field>(?:[^"]|\\")*)"$/
default | Cuando /^relleno lo siguiente:$/
default | Cuando /^selecciono "(?P<option>(?:[^"]|\\")*)" de "(?P<select>(?:[^"]|\\")*)"$/
default | Cuando /^adicionalmente selecciono "(?P<option>(?:[^"]|\\")*)" de "(?P<select>(?:[^"]|\\")*)"$/
default | Cuando /^marco "(?P<option>(?:[^"]|\\")*)"$/
default | Cuando /^desmarco "(?P<option>(?:[^"]|\\")*)"$/
default | Cuando /^adjunto el archivo "(?P<path>[^"]*)" a "(?P<field>(?:[^"]|\\")*)"$/
default | Entonces /^debo estar en "(?P<page>[^"]+)"$/
default | Entonces /^(?:|I )should be on (?:|the )homepage$/
default | Entonces /^la URL debe seguir el patrón (?P<pattern>"(?:[^"]|\\")*")$/
default | Entonces /^el código de estado de la respuesta debe ser (?P<code>\d+)$/
default | Entonces /^el código de estado de la respuesta no debe ser (?P<code>\d+)$/
default | Entonces /^debo ver "(?P<text>(?:[^"]|\\")*)"$/
default | Entonces /^no debo ver "(?P<text>(?:[^"]|\\")*)"$/
default | Entonces /^debo ver texto que siga el patrón (?P<pattern>"(?:[^"]|\\")*")$/
default | Entonces /^no debo ver texto que siga el patrón (?P<pattern>"(?:[^"]|\\")*")$/
default | Entonces /^la respuesta debe contener "(?P<text>(?:[^"]|\\")*)"$/
default | Entonces /^la respuesta no debe contener "(?P<text>(?:[^"]|\\")*)"$/
default | Entonces /^debo ver "(?P<text>(?:[^"]|\\")*)" en el elemento "(?P<element>[^"]*)"$/
default | Entonces /^no debo ver "(?P<text>(?:[^"]|\\")*)" en el elemento "(?P<element>[^"]*)"$/
default | Entonces /^el elemento "(?P<element>[^"]*)" debe contener "(?P<value>(?:[^"]|\\")*)"$/
default | Entonces /^the "(?P<element>[^"]*)" element should not contain "(?P<value>(?:[^"]|\\")*)"$/
default | Entonces /^debo ver un elemento "(?P<element>[^"]*)"$/
default | Entonces /^no debo ver un elemento "(?P<element>[^"]*)"$/
default | Entonces /^el campo "(?P<field>(?:[^"]|\\")*)" debe contener "(?P<value>(?:[^"]|\\")*)"$/
default | Entonces /^el campo "(?P<field>(?:[^"]|\\")*)" no debe contener "(?P<value>(?:[^"]|\\")*)"$/
default | Entonces /^debo ver (?P<num>\d+) "(?P<element>[^"]*)" elementos$/
default | Entonces /^la casilla de selección "(?P<checkbox>[^"]*)" debe estar marcada$/
default | Entonces /^the "(?P<checkbox>(?:[^"]|\\")*)" checkbox is checked$/
default | Entonces /^the checkbox "(?P<checkbox>(?:[^"]|\\")*)" (?:is|should be) checked$/
default | Entonces /^the "(?P<checkbox>(?:[^"]|\\")*)" checkbox should (?:be unchecked|not be checked)$/
default | Entonces /^the "(?P<checkbox>(?:[^"]|\\")*)" checkbox is (?:unchecked|not checked)$/
default | Entonces /^the checkbox "(?P<checkbox>(?:[^"]|\\")*)" should (?:be unchecked|not be checked)$/
default | Entonces /^the checkbox "(?P<checkbox>(?:[^"]|\\")*)" is (?:unchecked|not checked)$/
default | Entonces /^print current URL$/
default | Entonces /^imprime la última respuesta$/
default | Entonces /^muestra la última respuesta$/

Este listado muestra las posibles acciones que podemos usar en los test, y su sintaxis.

Escribiendo nuestros primeros test. Escenarios

Los escenarios describen la funcionalidad que queremos testear, tal y como si fuese un usuario final. Estos escenarios se escriben en unos archivos llamados fueatures y se alojan en la carpeta /features.

Ejemplo, queremos testear que un usuario anónimo en Drupal, puede iniciar y cerrar correctamente su sesión. En el front hemos habilitado un bloque que sólo verán los usuarios registrados, con un texto "BIenvenido usuario". El usuario anónimo no debería ver ese bloque, ni el de herramientas.

Creamos un fichero nuevo en /features:

home.feature

Feature: Testing Home Page content
As an user, I want to be able to test
the home page text

Scenario: Anonimous can't see private block on front, nor tools block, and can login and logut fine
Given I am on "/"
Then I should not see "usuarios registrados"
And I should not see "Herramientas"
And I should see "Bienvenido a Behat test"
Given I am on "/user/login"
When I fill in "edit-name" with "test-behat"
And I fill in "edit-pass" with "test-behat"
And I press "edit-submit"
Given I am on "/"
Then I should see "usuarios registrados"
And I should see "Mi cuenta"

Ahora, lanzamos el test, desde la raiz del proyecto

$ bin/behat

Feature: Testing Home Page content
  As an user, I want to be able to test
  the home page text

  Scenario: Anonimous can't see private block on front, nor tools block, and can login and logut fine # features/home.feature:5
    Given I am on "/"                                                                                 # Drupal\DrupalExtension\Context\MinkContext::visit()
    Then I should not see "usuarios registrados"                                                      # Drupal\DrupalExtension\Context\MinkContext::assertPageNotContainsText()
    And I should not see "Herramientas"                                                               # Drupal\DrupalExtension\Context\MinkContext::assertPageNotContainsText()
    And I should see "Bienvenido a Behat test"                                                        # Drupal\DrupalExtension\Context\MinkContext::assertPageContainsText()
    Given I am on "/user/login"                                                                       # Drupal\DrupalExtension\Context\MinkContext::visit()
    When I fill in "edit-name" with "test-behat"                                                      # Drupal\DrupalExtension\Context\MinkContext::fillField()
    And I fill in "edit-pass" with "test-behat"                                                       # Drupal\DrupalExtension\Context\MinkContext::fillField()
    And I press "edit-submit"                                                                         # Drupal\DrupalExtension\Context\MinkContext::pressButton()
    Given I am on "/"                                                                                 # Drupal\DrupalExtension\Context\MinkContext::visit()
    Then I should see "usuarios registrados"                                                          # Drupal\DrupalExtension\Context\MinkContext::assertPageContainsText()
    And I should see "Mi cuenta"                                                                      # Drupal\DrupalExtension\Context\MinkContext::assertPageContainsText()

1 escenario (1 pasaron)
11 pasos (11 pasaron)
0m1.10s (11.60Mb)

Como podemos ver, el escenario es válido y se valida el test completo sin aparecer errores (11 pasos / 11 pasaron).

Supongamos que accidentalmente cambiamos la configuración del bloque, y queda visible también para usuarios anónimos, lanzamos de nuevo el test, y mostraría lo siguiente:

Feature: Testing Home Page content
  As an user, I want to be able to test
  the home page text

  Scenario: Anonimous can't see private block on front, nor tools block, and can login and logut fine # features/home.feature:5
    Given I am on "/"                                                                                 # Drupal\DrupalExtension\Context\MinkContext::visit()
    Then I should not see "usuarios registrados"                                                      # Drupal\DrupalExtension\Context\MinkContext::assertPageNotContainsText()
      The text "usuarios registrados" appears in the text of this page, but it should not. (Behat\Mink\Exception\ResponseTextException)
    And I should not see "Herramientas"                                                               # Drupal\DrupalExtension\Context\MinkContext::assertPageNotContainsText()
    And I should see "Bienvenido a Behat test"                                                        # Drupal\DrupalExtension\Context\MinkContext::assertPageContainsText()
    Given I am on "/user/login"                                                                       # Drupal\DrupalExtension\Context\MinkContext::visit()
    When I fill in "edit-name" with "test-behat"                                                      # Drupal\DrupalExtension\Context\MinkContext::fillField()
    And I fill in "edit-pass" with "test-behat"                                                       # Drupal\DrupalExtension\Context\MinkContext::fillField()
    And I press "edit-submit"                                                                         # Drupal\DrupalExtension\Context\MinkContext::pressButton()
    Given I am on "/"                                                                                 # Drupal\DrupalExtension\Context\MinkContext::visit()
    Then I should see "usuarios registrados"                                                          # Drupal\DrupalExtension\Context\MinkContext::assertPageContainsText()
    And I should see "Mi cuenta"                                                                      # Drupal\DrupalExtension\Context\MinkContext::assertPageContainsText()

--- Escenarios fallidos:

    features/home.feature:5

1 escenario (1 fallaron)
11 pasos (1 pasaron, 1 fallaron, 9 saltadas)
0m0.41s (11.36Mb)

Y ahí tenemos visible el fallo, identificado perfectamente el escenario.

Conclusiones

Esta herramienta por tanto ayuda a adoptar buenas prácticas en los equipos de desarrollo, siendo muy recomendable la rutina de ejecutar los test antes de enviar un commit, para asegurarnos que el código no genera regresiones, incluso su integración con herramientas de integración continua como Jenkins.

Contacto

¿Te interesan nuestros servicios?

Contáctanos