Test automatizados en Drupal con Behat

10/02/2019
Enviado por pepem el Dom, 22/07/2018 - 14:47

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 usuario 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 pueden escribir los test de la forma: "Teniendo que ... Entonces debería ver...". Se puede además extender escribiendo funciones PHP personalizadas en el archivo FeatureContext.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 de un usuario en la web. Utiliza un lenguaje similar al inglés, humano y comprensible para escribir los pasos de Behat. por lo que además, puede ser creado, mantenido y entendido 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.

¿Donde no puede ayudar Behat?

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

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)
/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'

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 nuestro test. Escenarios

Los escenarios describen la funcionalidad que queremos testear, tal y como si fuese un usuario final. Estos escenarios se escriben en lenguaje Gherkin, en unos archivos llamados 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 dentro de la carpeta behat:

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 una carpeta superior a behat

$ 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 sin aparecen 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, identificando perfectamente el escenario.

Esta herramienta 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 el código no genera regresiones, incluso su integración con herramientas de integración continua como Jenkins.