Behat es una gran herramienta de testing para Drupal pero su configuración inicial puede ser un poco complicada de establecer. En este artículo voy a intentar indicar rápidamente como realizar una configuración básica con behat 2.x y Drupal Extension para echar andar los tests behat con Drupal. Para ello se deben completar los siguientes pasos:
Instalación de behat y Drupal Extension Instalar behat y Drupal Extension en el sistema. Hay varias formas de hacerlo y escapa al dominio de esta pequeña entrada. En la página de Drupal Extension hay instrucciones para realizar la instalación. Hay que tener en cuenta que es necesario PHP 5.3 o superior.
Fichero behat.yml Crear un fichero behat.yml en sites/default/ con un contenido similar a este:
default:
paths:
features: '../all/tests/features'
filters:
tags: "~@api&&~@drush"
extensions:
Behat\MinkExtension\Extension:
goutte: ~
selenium2: ~
base_url: 'http://miproyecto.me'
show_cmd: lynx %s
files_path: "path/to/files"
Drupal\DrupalExtension\Extension:
blackbox: ~
drush_driver: "drush"
drush:
root: "/ruta/a/raiz/drupal"
api_driver: "drupal"
drupal:
drupal_root: "/ruta/a/raiz/drupal"
region_map:
content: "#content"
footer: "#footer"
header: "#header"
header bottom: "#header-bottom"
navigation: "#navigation"
highlighted: "#highlighted"
help: "#help"
bottom: "#bottom"
selectors:
message_selector: '.messages'
error_message_selector: '.messages.error'
success_message_selector: '.messages.status'
warning_message_selector: '.messages.warning'
text:
password_field: "Contraseña"
username_field: "Correo electrónico"
log_in: "Entrar"
log_out: "Cerrar sesión"
Obviamente habrá que modificar base_url de "http://miproyecto.me" a la URL del proyecto que queremos testear. Igualmente, los valores "/ruta/a/raiz/drupal" deberán ser sustituidos por la ruta completa a la raíz de Drupal.
Por último, hay varias secciones a tener en cuenta:
region_map indica los selectores de las diferentes regiones, usados para los pasos que proporciona Drupal Extension que hacen referencia a regiones del theme. Por ejemplo, el paso I should see the link "link" in the "region name" hace uso de estos selectores para localizar en el HTMl la región correspondiente.
selectors guarda, de forma parecida a region_map, selectores relacionados con otros mecanismos estándar de Drupal. Por el momento tiene los que hacen referencia a los diferentes tipos de mensaje, aunque puede que se añadan más más adelante. Por ejemplo, el paso I should not see the warning message "warning message" usa el selector declarado en warning_message_selector para comprobar que el mensaje indicado no existe como mensaje de aviso.
En text se indican ciertos textos que sirven a Drupal Extension para realizar ciertas acciones. Por ejemplo, para hacer login Drupal Extension accede a user/login/ y busca los campos de usuario y contraseña mediante sus etiquetas y envía el formulario. Con text se pueden indicar cuales son estas etiquetas, algo necesario cuando se modifican las que Drupal trae por defecto. La lista de textos mostrada en el fichero de ejemplo no es exhaustiva y podría haber más.
Otra sección importante es path_files, dentro de Behat\MinkExtension\Extension. Aquí se indica de qué directorio debe behat coger los ficheros a adjuntar cuando se usa el paso I attach the file "mi_fichero.txt" to "File upload". Personalmente prefiero colocar los ficheros de prueba dentro de sites/all/tests/files, por lo que tendría:
files_path: "/mi_proyecto/sites/all/tests/files"
De esta forma para usar el paso anterior de ejemplo debería colocar en el directorio /mi_proyecto/sites/all/tests/files el fichero mi_fichero.txt para que el paso funcione correctamente.
Un dato importante para facilitar la depuración es show_cmd, dentro de la sección Behat\MinkExtension\Extension. Es el comando que Behat usará para mostrar visualmente la última respuesta recibida (el paso show last response). Esto es esencial para poder depurar pasos que no estén funcionado como uno espera. En este ejemplo se indica que se debe usar el navegador de consola lynx, dado que nuestros entornos de desarrollo están en vagrant, por lo que no solemos activar el sistema gráfico.
Crear la primera feature de prueba En el directorio sites/all/tests/features/ (que habrá que crear primero) se añade un fichero behat.feature con el siguiente contenido:
Feature: Testeo con behat
Para poder usar behat como herramienta de testeo
Como usuario
Debo poder lanzar tests de behat
Scenario: Comprobar acceso a la home
Given I go to "/"
Then the response status code should be 200
Crear directorio sites/all/tests/features/bootstrap/ Se debe crear el directorio sites/all/tests/features/bootstrap/. Es ahí donde behat buscará el fichero FeatureContext.php con la clase contexto propia para los tests. Los tests pueden ejecutarse sin dicha clase, pero por la configuración del behat.yml necesita que dicho directorio esté creado.
Bonus: clase FeatureContext Para añadir nuestros propios pasos podemos crear la clase FeatureContext. Inicialmente debería ser algo así como:
use Behat\Behat\Context\ClosuredContextInterface,
Behat\Behat\Context\TranslatedContextInterface,
Behat\Behat\Context\BehatContext,
Behat\Behat\Exception\PendingException;
use Behat\Gherkin\Node\PyStringNode,
Behat\Gherkin\Node\TableNode;
use Behat\Behat\Context\Step\Then;
use Behat\Behat\Context\Step\Given;
use Behat\Behat\Context\Step\When;
/**
*
*/
class FeatureContext extends Drupal\DrupalExtension\Context\DrupalContext {
}
Las claúsulas use no son todas necesarias, pero según se vayan añadiendo pasos propios probablemente se vayan necesitando.
Ejecutar nuestro primer escenario (test) Una vez realizados estos pasos podemos ejecutar la feature que hemos añadido antes para comprobar que behat funciona correctamente. Para ello hay que situarse en el directorio sites/default/, donde debería estar el fichero behat.yml creado anteriormente, y ejecutar:
behat
Fácil, ¿no? La salida de behat debería ser parecido a esto:
Feature: Testeo con behat
Para poder usar behat como herramienta de testeo
Como usuario
Debo poder lanzar tests de behat
Scenario: Comprobar acceso a la home
Given I go to "/"
Then the response status code should be 200
1 scenario (1 passed)
2 steps (2 passed)
0m2.98s
A veces pueden surgir problemas de inclusión de ficheros de Drupal si ejecutamos el driver Drupal de behat (el que permite usar la API de Drupal dentro de nuestros pasos). Probablemente se deba a alguna incorrección de ciertos módulos a la hora de incluir ficheros. La forma más sencilla de evitar el problema es ejecutar behat desde la raíz Drupal pasándole la ruta al fichero behat.yml:
behat --config sites/default/behat.yml
Ah, y por último, para ver los pasos disponibles debemos ejecutar (desde sites/default/ o desde otro sitio pero indicando el fichero behat.yml mediante el parámetor --config):
behat -dl
El resultado debería ser algo así como:
Given /^(?:that I|I) am at "(?P[^"]*)"$/
When /^I visit "(?P[^"]*)"$/
When /^I click "(?P<link>[^"]*)"$/
Given /^for "(?P<field>[^"]*)" I enter "(?P<value>[^"]*)"$/
Given /^I enter "(?P<value>[^"]*)" for "(?P<field>[^"]*)"$/
Given /^I wait for AJAX to finish$/
When /^(?:|I )press "(?P<button>(?:[^"]|\\")*)"$/
When /^(?:|I )press the "(?P<button>[^"]*)" button$/
Given /^(?:|I )press the "([^"]*)" key in the "([^"]*)" field$/
Then /^I should see the link "(?P<link>[^"]*)"$/
Then /^I should not see the link "(?P<link>[^"]*)"$/
Then /^I (?:|should )see the heading "(?P<heading>[^"]*)"$/
Then /^I (?:|should )not see the heading "(?P<heading>[^"]*)"$/
Then /^I should see the heading "(?P<heading>[^"]*)" in the "(?P<region>[^"]*)"(?:| region)$/
Then /^I should see the "(?P<heading>[^"]*)" heading in the "(?P<region>[^"]*)"(?:| region)$/
When /^I (?:follow|click) "(?P<link>[^"]*)" in the "(?P<region>[^"]*)"(?:| region)$/
Then /^I should see the link "(?P<link>[^"]*)" in the "(?P<region>[^"]*)"(?:| region)$/
Then /^I should not see the link "(?P<link>[^"]*)" in the "(?P<region>[^"]*)"(?:| region)$/
Then /^I should see (?:the text |)"(?P<text>[^"]*)" in the "(?P<region>[^"]*)"(?:| region)$/
Then /^I should not see (?:the text |)"(?P<text>[^"]*)" in the "(?P<region>[^"]*)"(?:| region)$/
Given /^I press "(?P<button>[^"]*)" in the "(?P<region>[^"]*)"(?:| region)$/
Given /^(?:|I )fill in "(?P<value>(?:[^"]|\\")*)" for "(?P<field>(?:[^"]|\\")*)" in the "(?P<region>[^"]*)"(?:| region)$/
Given /^(?:|I )fill in "(?P<field>(?:[^"]|\\")*)" with "(?P<value>(?:[^"]|\\")*)" in the "(?P<region>[^"]*)"(?:| region)$/
Then /^(?:I|I should) see the text "(?P<text>[^"]*)"$/
Then /^I should not see the text "(?P<text>[^"]*)"$/
Then /^I should get a "(?P<code>[^"]*)" HTTP response$/
Then /^I should not get a "(?P<code>[^"]*)" HTTP response$/
Given /^I check the box "(?P<checkbox>[^"]*)"$/
Given /^I uncheck the box "(?P<checkbox>[^"]*)"$/
When /^I select the radio button "(?P<label>[^"]*)" with the id "(?P<id>[^"]*)"$/
When /^I select the radio button "(?P<label>[^"]*)"$/
Given /^I am an anonymous user$/
Given /^I am not logged in$/
Given /^I am logged in as a user with the "(?P<role>[^"]*)" role$/
Given /^I am logged in as "(?P<name>[^"]*)"$/
Given /^I am logged in as a user with the "(?P<permission>[^"]*)" permission(?:|s)$/
Given /^I click "(?P<link>[^"]*)" in the "(?P<row_text>[^"]*)" row$/
Given /^the cache has been cleared$/
Given /^I run cron$/
Given /^I am viewing (?:a|an) "(?P<type>[^"]*)" node with the title "(?P<title>[^"]*)"$/
Given /^(?:a|an) "(?P<type>[^"]*)" node with the title "(?P<title>[^"]*)"$/
Given /^I am viewing my "(?P<type>[^"]*)" node with the title "(?P<title>[^"]*)"$/
Given /^"(?P<type>[^"]*)" nodes:$/
Given /^I am viewing (?:a|an) "(?P<type>[^"]*)" node:$/
Then /^I should be able to edit (?:a|an) "([^"]*)" node$/
Given /^I am viewing (?:a|an) "(?P<vocabulary>[^"]*)" term with the name "(?P<name>[^"]*)"$/
Given /^(?:a|an) "(?P<vocabulary>[^"]*)" term with the name "(?P<name>[^"]*)"$/
Given /^users:$/
Given /^"(?P<vocabulary>[^"]*)" terms:$/
Then /^I should see the error message(?:| containing) "([^"]*)"$/
Then /^I should see the following <error messages>$/
Given /^I should not see the error message(?:| containing) "([^"]*)"$/
Then /^I should not see the following <error messages>$/
Then /^I should see the success message(?:| containing) "([^"]*)"$/
Then /^I should see the following <success messages>$/
Given /^I should not see the success message(?:| containing) "([^"]*)"$/
Then /^I should not see the following <success messages>$/
Then /^I should see the warning message(?:| containing) "([^"]*)"$/
Then /^I should see the following <warning messages>$/
Given /^I should not see the warning message(?:| containing) "([^"]*)"$/
Then /^I should not see the following <warning messages>$/
Then /^I should see the message(?:| containing) "([^"]*)"$/
Then /^I should not see the message(?:| containing) "([^"]*)"$/
Given /^I run drush "(?P<command>[^"]*)"$/
Given /^I run drush "(?P<command>[^"]*)" "(?P<arguments>(?:[^"]|\\")*)"$/
Then /^drush output should contain "(?P<output>[^"]*)"$/
Then /^drush output should not contain "(?P<output>[^"]*)"$/
Then /^(?:|I )break$/
Given /^(?:|I )am on (?:|the )homepage$/
When /^(?:|I )go to (?:|the )homepage$/
Given /^(?:|I )am on "(?P<page>[^"]+)"$/
When /^(?:|I )go to "(?P<page>[^"]+)"$/
When /^(?:|I )reload the page$/
When /^(?:|I )move backward one page$/
When /^(?:|I )move forward one page$/
When /^(?:|I )follow "(?P<link>(?:[^"]|\\")*)"$/
When /^(?:|I )fill in "(?P<field>(?:[^"]|\\")*)" with "(?P<value>(?:[^"]|\\")*)"$/
When /^(?:|I )fill in "(?P<field>(?:[^"]|\\")*)" with:$/
When /^(?:|I )fill in "(?P<value>(?:[^"]|\\")*)" for "(?P<field>(?:[^"]|\\")*)"$/
When /^(?:|I )fill in the following:$/
When /^(?:|I )select "(?P<option>(?:[^"]|\\")*)" from "(?P<select>(?:[^"]|\\")*)"$/
When /^(?:|I )additionally select "(?P<option>(?:[^"]|\\")*)" from "(?P<select>(?:[^"]|\\")*)"$/
When /^(?:|I )check "(?P<option>(?:[^"]|\\")*)"$/
When /^(?:|I )uncheck "(?P<option>(?:[^"]|\\")*)"$/
When /^(?:|I )attach the file "(?P[^"]*)" to "(?P<field>(?:[^"]|\\")*)"$/
Then /^(?:|I )should be on "(?P<page>[^"]+)"$/
Then /^(?:|I )should be on (?:|the )homepage$/
Then /^the (?i)url(?-i) should match (?P<pattern>"(?:[^"]|\\")*")$/
Then /^the response status code should be (?P<code>\d+)$/
Then /^the response status code should not be (?P<code>\d+)$/
Then /^(?:|I )should see "(?P<text>(?:[^"]|\\")*)"$/
Then /^(?:|I )should not see "(?P<text>(?:[^"]|\\")*)"$/
Then /^(?:|I )should see text matching (?P<pattern>"(?:[^"]|\\")*")$/
Then /^(?:|I )should not see text matching (?P<pattern>"(?:[^"]|\\")*")$/
Then /^the response should contain "(?P<text>(?:[^"]|\\")*)"$/
Then /^the response should not contain "(?P<text>(?:[^"]|\\")*)"$/
Then /^(?:|I )should see "(?P<text>(?:[^"]|\\")*)" in the "(?P<element>[^"]*)" element$/
Then /^(?:|I )should not see "(?P<text>(?:[^"]|\\")*)" in the "(?P<element>[^"]*)" element$/
Then /^the "(?P<element>[^"]*)" element should contain "(?P<value>(?:[^"]|\\")*)"$/
Then /^the "(?P<element>[^"]*)" element should not contain "(?P<value>(?:[^"]|\\")*)"$/
Then /^(?:|I )should see an? "(?P<element>[^"]*)" element$/
Then /^(?:|I )should not see an? "(?P<element>[^"]*)" element$/
Then /^the "(?P<field>(?:[^"]|\\")*)" field should contain "(?P<value>(?:[^"]|\\")*)"$/
Then /^the "(?P<field>(?:[^"]|\\")*)" field should not contain "(?P<value>(?:[^"]|\\")*)"$/
Then /^the "(?P<checkbox>(?:[^"]|\\")*)" checkbox should be checked$/
Then /^the checkbox "(?P<checkbox>(?:[^"]|\\")*)" (?:is|should be) checked$/
Then /^the "(?P<checkbox>(?:[^"]|\\")*)" checkbox should not be checked$/
Then /^the checkbox "(?P<checkbox>(?:[^"]|\\")*)" should (?:be unchecked|not be checked)$/
Then /^the checkbox "(?P<checkbox>(?:[^"]|\\")*)" is (?:unchecked|not checked)$/
Then /^(?:|I )should see (?P<num>\d+) "(?P<element>[^"]*)" elements?$/
Then /^print current URL$/
Then /^print last response$/
Then /^show last response$/
Como complemento recomiendo echar un ojo a la documentación de Drupal Extension.