Este documento describe Behat y Mink, dos frameworks para Behavior Driven Development (BDD) en PHP. Behat permite escribir historias de usuario (features) en un lenguaje natural llamado Gherkin y mapearlas a pruebas automatizadas escritas en PHP. Mink permite controlar navegadores web para probar aplicaciones, integrándose con Behat. Juntos, Behat y Mink permiten probar el comportamiento completo de una aplicación web desde la perspectiva del usuario final.
8. Proyecto Open Source creado por Konstantin Kudryashov
Desarrollo albergado en GitHub: https://github.com/Behat/Behat
¿Qué es Behat?
9. ¿Qué es Behat?
Un framework para testing en BDD (Behaviour Driven Development)
Escrito en php y para php 5.3+
10. Inspirado en Cucumber, de Ruby (https://cucumber.io/)
Behat 3 está considerado como la versión oficial de la implementación de
Cucumber para php y muy bien considerado en el mundo del BDD
¿Qué es Behat?
11. Behat usa como lenguaje para contar historias: Gherkin
Gherkin es un DSL, Domain Specific Language, como SQL
¿Qué es Behat?
12. Gherkin usa un lenguaje humano real estructurado
Uno modo de escribir una historia que puede ser automatizada
¿Qué es Behat?
13. Las sentencias se pueden escribir en varios idiomas
NO es un lenguaje de programación
¿Qué es Behat?
14. Promueve la comunicación entre todas las partes que intervienen en la empresa,
como por ejemplo: la parte de negocio y la parte de desarrollo
¿Por qué Behat?
Ubiquitous Language (Martin Fowler): https://martinfowler.com/bliki/UbiquitousLanguage.html
UBIQUITOUS LANGUAGE
15. Feature: The Coffee Machine serves coffees
In order to take a coffee
As a buyer
I should be able to buy a coffee to stay awake
Scenario: Buy one coffee
Given Coffee Machine can serve up to 10 coffees at $0.60
When I deposit 1 dollar
And I press the coffee button
Then I should be served a coffee
And I should be returned $0.40
And There should be 9 coffees left
Vamos a contar una historia
16. Feature: The Coffee Machine serves coffees
In order to take a coffee
As a buyer
I should be able to buy a coffee to stay awake
Scenario: Buy one coffee
Given Coffee Machine can serve up to 10 coffees at $0.60
When I deposit 1 dollar
And I press the coffee button
Then I should be served a coffee
And I should be returned $0.40
And There should be 9 coffees left
QUÉ
POR QUÉ
QUIEN
rol de usuario
17. Feature: The Coffee Machine serves coffees
In order to take a coffee
As a buyer
I should be able to buy a coffee to stay awake
Scenario: Buy one coffee
Given Coffee Machine can serve up to 10 coffees at $0.60
When I deposit 1 dollar
And I press the coffee button
Then I should be served a coffee
And I should be returned $0.40
And There should be 9 coffees left
1 única Feature (Historia) por archivo .feature
N Scenarios por Feature
18. Feature: The Coffee Machine serves coffees
In order to take a coffee
As a buyer
I should be able to buy a coffee to stay awake
Scenario: Buy one coffee
Given Coffee Machine can serve up to 10 coffees at $0.60
When I deposit 1 dollar
And I press the coffee button
Then I should be served a coffee
And I should be returned $0.40
And There should be 9 coffees left
Sentences:
Acciones/Steps
a testear
19. Feature: The Coffee Machine serves coffees
In order to take a coffee
As a buyer
I should be able to buy a coffee to stay awake
Scenario: Buy one coffee
Given Coffee Machine can serve up to 10 coffees at $0.60
When I deposit 1 dollar
And I press the coffee button
Then I should be served a coffee
And I should be returned $0.40
And There should be 9 coffees left
precondiciones/
inicializaciones
interacción
de usuario
resultados
20. ¿Dónde ocurre la magia?
¿Y dónde está la implementación de los tests?
21. Behat “mapea” definiciones Gherkin con implementación Php y
las relaciona mediante anotaciones
Scenario Steps
.feature
acciones en Gherkin
Step Definitions
FeatureContext.php
Código PHP
22. /**
* @When I deposit 1 dollar
*/
public function iDepositOneDollar()
{
// do something
}
Feature: The Coffee Machine serves coffees
...
Scenario: Buy one coffee
Given ...
When I deposit 1 dollar
And ...
gherkin
.feature
FeatureContext.php
anotación + método
24. 4.- Inicializar Behat para configurar un framework base: ./bin/behat --init
5.- Tras crear los tests ejecutar ./bin/behat
6.- Opcionalmente crear archivo behat.yml (configuración avanzada)
bin
composer.json
composer.lock
features
|_ bootstrap
|_ FeatureContext.php
vendor ...
Ubicación para los
archivos .feature
Ubicación archivos
*Context.php
Configuración proyecto básico Behat
25. Una puede definir uno o más Scenario
Cada testea un comportamiento
Cada puede definir uno o más Steps
Los Steps se pueden encadenar
Los Comentarios están permitidos
Given When And/But Then
Scenario
Scenario
Feature
# this is a comment
Qué define cada Keyword
26. Scenario Outline
Background
Given ( + table
transformation )
Examples ( + table )
Más definiciones de Keywords
Se ejecuta el mismo
Scenario para cada fila
de la tabla
Todos los Steps dentro
de Background se
iniciarán antes de cada
Scenario
Nos permite inicializar
datos para trabajar
dentro de un Scenario
27. Escritura de una Feature: GOOD PRACTICE
Explica QUÉ es lo que se desea hacer y NO CÓMO de modo que
todo el mundo pueda entender la situación
Pensar 2 veces antes de escribir una frase de modo que
cualquiera pueda entender el caso *
* caso usb
28. Indicar expresamente el valor de un elemento de formulario
Utilizar en una sentencia el valor de un id
Bad: I press the button with id “server-coffee”
Escritura de una Feature: BAD PRACTICE
29. Good: I press the coffee button
Escritura de una Feature: GOOD PRACTICE
Indicar cuál es la acción que se desea llevar a cabo, no cómo
45. Se integra bien con “Navegadores” Web gracias a: Selenium & Sahi
Sahi package abandonado y sin mantener: https://packagist.org/packages/behat/mink-sahi-driver
¿Por qué Mink?
47. Se puede integrar fácilmente con frameworks para Php tales como
Symfony/Laravel
¿Por qué Mink?
48. Ofrece un API común para todos los diferentes Emuladores Web
mediante la definición de una interfaz (contrato)
¿Por qué Mink?
49. 2 Tipos de Emuladores Web
1.- Emuladores Web Headless (sin cabeza)
2.- Controladores Web (Browser Controllers)
50. Son simplemente implementaciones de especificaciones HTTP
Emuladores Web Headless
No se puede testear JS Rápidos (Goutte)
cUrl o Guzzle
51. Su objetivo es controlar un navegador web real
Controladores Web (Browser Controllers)
SI que puede testear JS! Lento (Selenium/Sahi)
52. ¿Cómo funciona Mink con Selenium?
Selenium2
Chrome
Gecko
En otra terminal
se inicia una
instancia de
Selenium Server
Standalone (.jar)
el cual
Interacciona con
los motores de
navegadores
reales
directamente o vía
drivers específicosMink inicia una
sesión con un
driver específico
Este driver a su
vez se comunica
con la
aplicación/driver
específica del
navegador web
1
2
3
56. ¿Por qué Behat y Mink?
La ecuación permite testear
el comportamiento de un proceso web completo
57. El binomio actúa como intermediario
Product Managers y
clientes pueden contar una
historia (mediante un
lenguaje estructurado)
De modo que los
desarrolladores pueden
entender fácilmente los
requerimientos e
implementarlos
¿Por qué Behat y Mink?
58. Es un modo natural de validar los
requerimientos
¿Por qué Behat y Mink?
Promueve escribir los tests antes de escribir
una línea de código
62. Navegando por Wikipedia
Visitar la página de Wikipedia en Inglés
Buscar la definición de Behat
Guardar las referencias en un archivo de texto
¿Qué se pretende
66. default | When /^(?:|I )go to (?:|the )homepage$/
default | Then /^(?:|I )should be on "(?P<page>[^"]+)"$/
default | When /^(?:|I )follow "(?P<link>(?:[^"]|")*)"$/
default | When /^(?:|I )press "(?P<button>(?:[^"]|")*)"$/
default | Then /^(?:|I )should see "(?P<text>(?:[^"]|")*)"$/
default | Then /^(?:|I )should not see "(?P<text>(?:[^"]|")*)"$/
default | When /^(?:|I )fill in "(?P<field>(?:[^"]|")*)" with "(?P<value>(?:[^"]|")*)"$/
default | Then /^print current URL$/
default | Then /^print last response$/
default | When /^(?:|I )select "(?P<option>(?:[^"]|")*)" from "(?P<select>(?:[^"]|")*)"$/
default | When /^(?:|I )check "(?P<option>(?:[^"]|")*)"$/
default | Then /^the "(?P<checkbox>(?:[^"]|")*)" checkbox should be checked$/
default | Then /^(?:|I )should see "(?P<text>(?:[^"]|")*)" in the "(?P<element>[^"]*)" element$/
Algunas Step Definitions que nos ofrece gratis Mink vía MinkContext:
$ ./bin/behat -dl
Nota: Otros contextos interesantes: Behatch, StepThroughExtension, PageObjectExtension
70. Feature: Scrap behat references from wikipedia
In order get behat references
As an anonymous user
I visit wikipedia behat entry and scrap the references list
@javascript
Scenario: Scrap references
Given I am on "https://www.wikipedia.org" #Given I go to homepage
Then I should see "The Free Encyclopedia"
When I follow "English"
Then the url should match "wiki/Main_Page"
And I should see "Welcome to Wikipedia,"
When I fill in "Search Wikipedia" with "behat computer"
And I press "Search Wikipedia"
And print current URL #Debug
And I follow "Behat (computer science)"
Then I should see "Behat is intended to aid communication between"
And I save references in a local storage device#Sentencia a implementar
Archivo .feature: features/wikipedia.scrapper.feature
71. use BehatMinkExtensionContextRawMinkContext;
class FeatureContext extends RawMinkContext implements Context, SnippetAcceptingContext
/**
* @Given /^I save references in a local storage device$/
*/
public function iSaveReferencesInALocalStorageDevice ()
{
…
}
FeatureContext.php
Requerido para obtener una
Session de Mink
72. /** @Given /^I save references in a local storage device$/ */
public function iSaveReferencesInALocalStorageDevice ()
{
...
}
Step Definition
propio
Método que guarda las preferencias. Paso a paso
73. /** @Given /^I save references in a local storage device$/ */
public function iSaveReferencesInALocalStorageDevice ()
{
$page = $this->getSession()->getPage();
...
}
Obtener el
objeto Page
Obtener el objeto
Session (pensar en
una pestaña del
navegador)
Método que guarda las preferencias. Paso a paso
74. /** @Given /^I save references in a local storage device$/ */
public function iSaveReferencesInALocalStorageDevice ()
{
$page = $this->getSession()->getPage();
$content = $page->find('named', array('id', 'mw-content-text' ));
}
named Selector
buscar por id
Método que guarda las preferencias. Paso a paso
75. /** @Given /^I save references in a local storage device$/ */
public function iSaveReferencesInALocalStorageDevice ()
{
...
$references = $content->find('css', '.references');
$items = $references->findAll('css', 'li');
}
Selectores CSS
¡Atención con el punto!
find vs findAll
Método que guarda las preferencias. Paso a paso
76. /** @Given /^I save references in a local storage device$/ */
public function iSaveReferencesInALocalStorageDevice ()
{
...
$links = array();
foreach ($items as $item) {
$linkContainer = $item->find('xpath', '//span[@class="reference-text"]' );
$links[] = $linkContainer ->find('xpath', '//a/@href')->getText();
}
}
Selectores xpath
obtener sólo el valor del enlace
Método que guarda las preferencias. Paso a paso
77. /** @Given /^I save references in a local storage device$/ */
public function iSaveReferencesInALocalStorageDevice ()
{
...
file_put_contents ('scrapped_references.txt' , join(PHP_EOL, $links));
}
Guardar el archivo en disco
Método que guarda las preferencias. Paso a paso
78. /** @Given /^I save references in a local storage device$/ */
public function iSaveReferencesInALocalStorageDevice ()
{
$page = $this->getSession()->getPage();
$content = $page->find('named', array('id', 'mw-content-text' ));
$references = $content->find('css', '.references');
$items = $references->findAll('css', 'li');
$links = array();
foreach ($items as $item) {
$linkContainer = $item->find('xpath', '//span[@class="reference-text"]' );
$links[] = $linkContainer ->find('xpath', '//a/@href')->getText();
}
file_put_contents ('scrapped_references.txt' , join(PHP_EOL, $links));
}
2.- Css Selectors
3.- Xpath Selectors
1.- Named Selector
Método que guarda las preferencias. Paso a paso
79. : implementar la misma solución con menor código
Vamos a Refactorizar
80. /** @Given /^I save references in a local storage device again$/ */
public function iSaveReferencesInALocalStorageDeviceAgain ()
{
$closure = function($item) { return $item->getText(); }; // anonymous function
$xpath = '//span[@class="reference-text"]/a/@href';
$links = array_map($closure, $this->findAll('xpath', $xpath));
file_put_contents ('scrapped_references_again.txt' , join(PHP_EOL, $links));
}
public function __call($method, $parameters)
{
$page = $this->getSession()->getPage();
if (method_exists($page, $method)) {
return call_user_func_array (array($page, $method), $parameters);
}
}
Magic Call
Sólo un selector xpath
Método que guarda las preferencias. Solución corta
Closure vs Lambda: https://www.ibm.com/developerworks/library/os-php-5.3new2/os-php-5.3new2-pdf.pdf
82. bdd# ./bin/behat
Feature: Scrap behat references from wikipedia
...
Scenario: Scrap References # features/wikipedia.scrapper.feature:6
Given I am on " https://www.wikipedia.org" # BehatMinkExtensionContextMinkContext::visit()
Then I should see " The Free Encyclopedia" # Behat...MinkContext::assertPageContainsText()
When I follow " English" # Behat...MinkContext::clickLink()
Then the url should match "wiki/Main_Page" # Behat...MinkContext::assertUrlRegExp()
And I should see " Welcome to Wikipedia," # Behat...MinkContext::assertPageContainsText()
When I fill in " Search Wikipedia" with "behat computer" # Behat...MinkContext::fillField()
And I press " Search Wikipedia" # Behat...MinkContext::pressButton()
And I follow " Behat (computer science)" # Behat...MinkContext::clickLink()
Then I should see " Behat is intended to aid communication between developers, clients and other
stakeholders during a..." # Behat...MinkContext::assertPageContainsText()
And I save references in a local storage device
# FeatureContext::iSaveReferencesInALocalStorageDevice()
1 scenario (1 passed)
10 steps (10 passed)
0m4.15s (15.36Mb)
84. Echando una mano a Mink
El flujo de ejecución de Behat está basado en eventos
Behat provee 8 Hooks de gran ayuda. Principio DRY
Los eventos se “mapean” mediante anotaciones
Las anotaciones simplifican la gestión de Datafixtures
Behat ofrece tags predefinidos (@javascript) y admite tags personalizados
86. 1) Java JRE
2) Firefox, Chrome y/o otros drivers. Ver sección Third Party Drivers, Bindings,
and Plugins at http://www.seleniumhq.org/download)
3) Selenium Standalone Server (http://www.seleniumhq.org/)
4) xvfb (X Virtual Frame Buffer). Permite crear una “gráfica virtual” en memoria.
5) Servidor web integrado por PHP (bin/console server:run) o un Servidor web
con urlRewrite (Apache/nginx) y host debidamente configurado.
Imprescindibles para ejecutar los tests:
Nota: Firefox v.46 con selenium-server-standalone-2.53.1.jar funciona sin problemas
87. En resumen:
$ ./bin/behat --config ./app/config/behat.yml
$ ./bin/console server:start
$ java -jar ./bin/selenium-server-standalone-2.53.1.jar (see ./scripts/start-selenium.sh)
$ sudo Xvfb :10 -ac
Y finalmente:
Imprescindibles para ejecutar los tests:
89. Caso de Uso www.takeachef.com
I follow “Empezar”
Existe otro “EMPEZAR”
más abajo, se actuará
sobre el primero que
encuentre
90. I follow “EMPEZAR”
Internamente en findAll, dentro de Element.php se
monta el siguiente xpath:
//html/.//a[./@href][((./@id = 'EMPEZAR' or
normalize-space(string(.)) = 'EMPEZAR' or ./@title = 'EMPEZAR'
or ./@rel = 'EMPEZAR') or .//img[./@alt = 'EMPEZAR'])] |
//html/.//*[translate(./@role,'ABCDEFGHIJKLMNOPQRSTUVWX
YZ', 'abcdefghijklmnopqrstuvwxyz') = 'link'][((./@id = 'EMPEZAR'
or ./@value = 'EMPEZAR') or ./@title = 'EMPEZAR' or
normalize-space(string(.)) = 'EMPEZAR')]
Caso de Uso
91. Caso de Uso
El driver que utilizamos en
Mink es Selenium2
Éste a su vez interactúa
con el driver de Firefox.
Utilizaremos la anotación
@javascript ya que el
asistente sólo funciona con
Javascript
En Selenium Server
Standalone Server 3.* el
driver de Firefox a utilizar es
Gecko
92. Caso de Uso
I should see “Encuentra tu Chef”
I should see “ENCUENTRA TU CHEF”
El tratamiento interno que hace Mink no
siempre es el mismo. En este caso
MinkContext llama al método de
pagetTextContains el cual busca el texto
mediante una expresión regular
preg_match
93. Caso de Uso
I click “Continuar”
Debido a un efecto delay en css:
- Hay un hook en BeforeStep,
que espera a que el contenedor
esté del todo visible, utiliza
función spins (alternativa: wait)
- Hay creado 1 Step Definition
propio para que busque el texto
sobre el contenedor que esté
activo y visible
94. Para interacciones donde la respuesta no es inmediata
Mink ofrece el método wait
$this->getSession()->wait($seconds);
No se considera una buena práctica la espera de
un número determinado de segundos
95. Pasar una evaluación Javascript es mucho mejor
La comprobación se realiza cada 100 ms hasta un tiempo máximo
expresamente definido
// wait for n milliseconds until JS expression becomes true:
$session->wait(
5000,
"$('.suggestions-results').children().length"
);
Espera a que la expresión javascript
retorne verdadero hasta 5 segundos
96. public function spins($closure, $seconds = 5, $fraction = 4)
{
$max = $seconds * $fraction;
$i = 1;
while ($i++ <= $max) {
if ($closure($this)) {
return true;
}
$this->getSession()->wait(1000 / $fraction);
}
$backtrace = debug_backtrace();
throw new Exception(
sprintf("Timeout thrown by %s::%s()n%s, line %s" ,
$backtrace[0]['class'], $backtrace[0]['function'],
$backtrace[0]['file'], $backtrace[0]['line']
)
);
}
Timeout 5s por defecto
Fracción de segundo
En casos más complejos o por claridad de código. Función “propia” spins:
97. Caso de Uso
I fill in the address “Castellón”
En este caso:
- Hay creado 1 Step Definition
propio para gestionar las
direcciones que ofrece la Api de
GoogleMaps.
- También se contempla la función
Spins ya que la carga de datos del
desplegable es asíncrona y
requiere un tiempo indeterminado
98. Caso de Uso
I click on “2 personas”
I click on “13+ personas desde 35€”
fallará ya que el texto está incluido
en 2 elementos distintos
99. Caso de Uso
I click on “Quiero dejarme”
I click on “wizard.preferences.surprise”
No hace falta escribir todo el texto
“Quiero dejarme sorprender” aunque
es aconsejable
No usar nunca la key de traducciones
e internamente llamar al servicio
translate o similar del framework
utilizado, puede dar falsos positivos
100. Caso de Uso
I click on “Enviar”
Finalizamos el proceso
Aparecerá un loader, en este caso
no hace falta utilizar la función
spins ya que no utilizamos ajax,
aunque lo pueda parecer.
Mink se encarga de esperar a que
se cargue la página
correspondiente al enlace clicado.
101. Caso de Uso
I should see “¡Nuestros chefs ya están
cocinando tu menú!
Creamos un Step Definition propio y
comprobamos que en base de datos se
guardan los datos que hemos introducido en
el test correctamente:
- Solicitud con los datos del test
- Creación de usuario
- Envío de e-mails de confirmación
- Derivación a Chefs (dados de alta
anteriormente mediante DataFixtures)
Práctica discutible, en ocasiones vale la pena ser prácticos