Registro de datos en MySql, Ajax y Behat
Escribiremos una aplicación web simple - una calculadora. Usándola como ejemplo, mostraremos cómo configurar selenium con behat y realizar pruebas automatizadas en ella.
Daniel Gustaw
• 14 min read
Descripción del Proyecto
Este es un proyecto que escribí mientras aprendía a usar una base de datos en PHP. Hace unos días, lo actualicé, añadí pruebas y decidí compartirlo.
En este artículo, aprenderás a centralizar la configuración del proyecto, registrar eventos en el sitio en la base de datos y probar el sitio utilizando selenium.
El código fuente consiste en:
PHP 43.2% Perl 19.8% HTML 19.6% Cucumber 7.4% JavaScript 6.5% CSS 3.5%
Después de escribir, el proyecto se verá así:
Instalación
¡Nota! Antes de ejecutar install.pl, asegúrate de que no tengas una base de datos llamada calc y chrome en sources.list. Los scripts de instalación en perl y bash no son largos; familiarízate con ellos antes de ejecutarlos.
Recomiendo realizar la instalación del proyecto en una máquina virtual, p.ej.: Lubuntu
.
Para instalar el proyecto, necesitas descargar el repositorio (en una ubicación donde no haya un directorio calc
)
git clone https://github.com/gustawdaniel/calc
Ve al directorio calc
e instala el software requerido. Antes de la instalación, revisa el archivo install.sh
y comenta la adición del repositorio de chrome si ya lo tienes instalado.
cd calc && bash install.sh
Verifica los parámetros de conexión de tu base de datos para mysql
. Si presionaste enter
durante la instalación y no tenías instalado previamente el paquete mysql-server
, puedes dejar los valores predeterminados. De lo contrario, ingresa los valores correctos en el archivo config/parameters.yml
y elimínalo del repositorio.
git rm --cached config/parameters.yml
Para instalar la base de datos y iniciar el servidor php, ingrese el comando
perl install.pl
En la nueva terminal (ctrl+n
), inicia el servidor de selenium.
selenium-standalone start
En el siguiente, puedes habilitar pruebas:
vendor/bin/behat
También puedes usar normalmente el sitio que está expuesto en el puerto 9000.
firefox localhost:9000
Si tienes los parámetros de conexión predeterminados a la base de datos, para ver el contenido de la base de datos escribe
sudo mysql -u root
use calc;
select * from log;
Estructura de Base de Datos
Normalmente empiezo un proyecto con la base de datos. Coloco su instalación en el archivo sql/main.sql
.
DROP DATABASE IF EXISTS database_name;
CREATE DATABASE IF NOT EXISTS database_name
DEFAULT CHARACTER SET = 'utf8'
DEFAULT COLLATE 'utf8_unicode_ci';
USE database_name;
CREATE TABLE log
(
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
time DATETIME NOT NULL,
a DOUBLE ,
b DOUBLE ,
button ENUM('sum', 'diff') ,
useragent VARCHAR(255)
);
Configuración
config:
host: 'localhost'
user: 'root'
pass: ''
base: 'calc'
port: '3306'
Nos referiremos a ellos en el instalador escrito en Perl y en la clase responsable de guardar en la base de datos en PHP.
Configuración en Perl
Escribiremos dos scripts - para crear y restablecer la base de datos. Usaremos la biblioteca YAML::Tiny
para leer el archivo parameters.yml
. El siguiente script:
Lee el archivo con parámetros en la variable $yaml
.
Guarda todos los parámetros en las variables correspondientes.
install.pl
#!/bin/perl
use YAML::Tiny;
use strict;
use warnings;
#
# Config:
#
my $yaml = YAML::Tiny->read( 'config/parameters.yml' );
my $baseName = $yaml->[0]->{config}->{base};
my $user = $yaml->[0]->{config}->{user};
my $pass = $yaml->[0]->{config}->{pass};
my $host = $yaml->[0]->{config}->{host};
my $port = $yaml->[0]->{config}->{port};
Crea variables con configuraciones de directorio. (Las instrucciones para crear la base de datos se encuentran en el archivo main.sql
.)
#
# Catalogs structure:
#
my $build = "build/";
my $sql = "sql/";
my $mainSQL = "main.sql";
Abre un archivo con código sql
y guarda el contenido en la variable $content
.
#
# Script:
#
#----------------------------------------- Database -------------#
# Prepare catalog
system('mkdir -p '.$build);
# Read file with mysql
my $content;
open(my $fh, '<', $sql.$mainSQL) or die "cannot open file";
{
local $/;
$content = <$fh>;
}
close($fh);
Reemplaza cada ocurrencia de la cadena database_name
con el nombre del archivo parameters.yml
y lo guarda.
# Replace database name by name from config
$content =~ s/database_name/$baseName/g;
# Save file with correct db name
open($fh, '>', $build.$mainSQL) or die "Could not open file' $!";
{
print $fh $content;
}
close $fh;
Otorga al usuario predeterminado el derecho a abrir la base de datos como root, crea la base de datos y inicia el servidor php
.
# Execute file
my $passSting = ($pass eq "") ? "" : " -p ".$pass;
system('sudo mysql -h '.$host.' -P '.$port.' -u '.$user.$passSting.' < '.$build.$mainSQL);
# Start server
system('cd web && php -S localhost:9000');
Configuración en PHP
Para manejar el archivo de configuración en php
, utilizaremos la biblioteca "mustangostang/spyc": "^0.6.1"
. Solo se usará al conectar a la base de datos - en el archivo php/DataBase.php
.
php/DataBase.php
<?php
require_once __DIR__."/../vendor/mustangostang/spyc/Spyc.php";
class DataBase
{
...
// config from yml
$config = Spyc::YAMLLoad(__DIR__."/../config/parameters.yml")["config"];
// connecting
$mysqli = @new mysqli($config["host"], $config["user"], $config["pass"], $config["base"], $config["port"]);
...
En la variable $config
, se almacena un array con parámetros para conectarse a la base de datos. El principio de funcionamiento es el mismo que en el script anterior.
Registro de Datos en la Base de Datos
En la sección sobre la estructura de la base de datos, mostramos qué registros contiene la única tabla que tenemos - log
. Estos son id
, time
, a
, b
, button
y useragent
. a
y b
corresponden a los números ingresados por el usuario. button
es la acción elegida, ya sea sum
para la suma o diff
para la diferencia. useragent
contiene datos sobre el navegador.
Ahora mapearemos el registro de la base de datos en php
como un objeto. Para ello, creamos una clase Log
en el archivo php/Log.php
php/Log.php
<?php
class Log
{
private $a;
private $b;
private $action;
private $agent;
/**
* @return mixed
*/
public function getC()
{
if($this->action=="sum"){
return $this->a + $this->b;
} elseif ($this->action=="diff") {
return $this->a - $this->b;
} else {
return null;
}
}
...
}
Contiene todos los campos de la tabla, excepto el identificador y la marca de tiempo, que se asignan durante la escritura en la base de datos. Marqué todos los getters y setters de las propiedades de la clase con tres puntos. En la mayoría de los IDE, pueden generarse automáticamente, por ejemplo, en PhpStorm
seleccionando código->Generar...
. El método getC
permite calcular el valor de suma o diferencia en el lado del servidor, que se utiliza más tarde en la interfaz API
.
Ahora podemos presentar en su totalidad la mencionada clase DataBase
, que se utilizó para guardar los datos recibidos de la página en la base de datos.
php/DataBase.php
<?php
require_once __DIR__."/Log.php";
require_once __DIR__."/../vendor/mustangostang/spyc/Spyc.php";
class DataBase
{
function save(Log $log){
$a = $log->getA();
$b = $log->getB();
$s = $log->getAction();
$u = $log->getAgent();
// config from yml
$config = Spyc::YAMLLoad(__DIR__."/../config/parameters.yml")["config"];
// connecting
$mysqli = @new mysqli($config["host"], $config["user"], $config["pass"], $config["base"], $config["port"]);
// test of connecting
if ($mysqli -> connect_errno)
{
$code = $mysqli -> connect_errno;
$mess = $mysqli -> connect_error;
die("Failed to connect to MySQL: ($code) $mess\n");
}
// definition of query
$query = 'INSERT INTO log VALUES(NULL,NOW(),?,?,?,?);';
// preparing
$stmt = @$mysqli -> prepare($query);
// test of preparing
if(!$stmt)
{
$code = $mysqli -> errno;
$mess = $mysqli -> error;
$mysqli -> close();
die("Failed to prepare statement: ($code) $mess\n");
}
// binding
$bind = @$stmt -> bind_param("ddss", $a, $b, $s, $u);
// test of binding
if(!$bind)
{
$stmt -> close();
$mysqli -> close();
die("Failed to bind param.\n");
}
// executing query
$exec = @$stmt -> execute();
// checking fails
if(!$exec)
{
$stmt -> close();
$mysqli -> close();
die("Failed to execute prepare statement.\n");
}
// clearing and disconnecting
$stmt -> close();
$mysqli -> close();
}
}
Esta clase no tiene propiedades, pero tiene un método - save
. Este método toma un objeto Log
y registra todas las propiedades de este objeto en la base de datos, agregando la hora también. La parte más interesante de esta clase - la obtención de la configuración se discutió anteriormente. El resto es solo una escritura regular en la base de datos.
Estas fueron clases, ahora es el momento del script de entrada del back-end de nuestra aplicación. Se encuentra en el archivo web/api.php
y es responsable de interceptar correctamente la solicitud, obtener parámetros, pasarlos a la base de datos y devolver una respuesta que contenga el resultado de la operación.
<?php
// error display
//ini_set('display_errors', 1);
//ini_set('display_startup_errors', 1);
//error_reporting(E_ALL);
require_once __DIR__."/../php/Log.php";
require_once __DIR__."/../php/DataBase.php";
// routing
if($_SERVER['REQUEST_METHOD']=="POST"
&& parse_url($_SERVER["REQUEST_URI"])["path"]=="/api.php/action"){
// get data from request
$log = new Log();
$log->setA($_POST["a"]);
$log->setB($_POST["b"]);
$log->setAction($_POST["action"]);
$log->setAgent($_SERVER['HTTP_USER_AGENT']);
// connect to db and save data
$db = new DataBase();
$db->save($log);
// send response
header('Content-type: application/json');
echo json_encode([
"a"=>$log->getA(),
"b"=>$log->getB(),
"c"=>$log->getC(),
"action"=>$log->getAction()
]);
}
Pruebas de Api con httpie
Podemos probar nuestra api
usando httpie
. Comando
http -fv 127.0.0.1:9000/api.php/action a=1 b=2 action="sum"
debería producir la siguiente salida:
POST /api.php/action HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 18
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Host: 127.0.0.1:9000
User-Agent: HTTPie/0.9.2
a=1&b=2&action=sum
HTTP/1.1 200 OK
Connection: close
Content-type: application/json
Host: 127.0.0.1:9000
X-Powered-By: PHP/7.0.8-0ubuntu0.16.04.3
{
"a": "1",
"action": "sum",
"b": "2",
"c": 3
}
AJAX
Cuando tenemos una base de datos lista y scripts para manejarla, no hay nada que nos impida completar el proyecto escribiendo el front end. Asumimos que la instalación fue exitosa y bower
instaló los paquetes necesarios - es decir, "bootstrap": "v4.0.0-alpha.5"
en el directorio web
. Dado que jQuery
es una dependencia para Bootstrap
, podemos usarlo al crear scripts.
Nuestro front end consiste en tres archivos: web/index.html
, web/css/style.css
y web/js/site.js
. Aquí están:
web/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Php calculator logging requests into database.</title>
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css">
<link href="https://fonts.googleapis.com/css?family=Lato:300" rel="stylesheet">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<section>
<div class="container">
<div class="row">
<div class="offset-md-3 col-md-6">
<div class="card text-xs-center">
<div class="card-header">
Set two numbers and chose calculation
</div>
<div class="card-block">
<div class="form-group">
<input id="a" type="number" step="any" class="form-control">
</div>
<div class="form-group">
<input id="b" type="number" step="any" class="form-control">
</div>
<div class="form-group row submit-area">
<div class="col-xs-6">
<input class="btn btn-lg btn-block hidden-xs-down btn-primary" type="submit" value='Sum' name="sum">
<input class="btn btn-lg btn-block hidden-sm-up btn-primary" type="submit" value='+' name="sum">
</div>
<div class="col-xs-6">
<input class="btn btn-lg btn-block hidden-xs-down btn-danger" type="submit" value='Difference' name="diff">
<input class="btn btn-lg btn-block hidden-sm-up btn-danger" type="submit" value='-' name="diff">
</div>
</div>
<div class="form-group">
<input id="c" type="text" readonly step="any" class="form-control">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<nav class="navbar navbar-fixed-bottom navbar-light bg-faded">
<a class="navbar-brand" href="README.html">Documentation</a>
<a class="navbar-brand float-xs-right" href="http://gustawdaniel.pl">Daniel Gustaw</a>
</nav>
<script src="bower_components/jquery/dist/jquery.min.js"></script>
<script src="js/site.js"></script>
</body>
</html>
Archivo HTML estándar. Lo que es interesante es el uso de la clase card
de bootstrap 4
y el cambio de textos de los botones de nombres completos a los símbolos +
y -
en anchos de pantalla pequeños.
Aún más simples son los estilos de nuestro sitio web.
web/css/style.css
body {
font-family: 'Lato', 'SansSerif', serif;
}
section {
margin-top: 20vh;
}
Esto se debe a Bootstrap, que realmente puede replicar mucho como yo esperaría. Lo único que necesitamos es margen vertical y fuente.
La parte más interesante es JavaScript:
web/js/site.js
(function () {
var submitArea = document.getElementsByClassName("submit-area")[0];
var card = document.getElementsByClassName("card")[0];
var a = document.getElementById("a");
var b = document.getElementById("b");
var c = document.getElementById("c");
function round(value,dec=5) {
return 1*(Math.round(value+"e+"+dec)+"e-"+dec);
}
submitArea.addEventListener('click',function (e) {
if(e.target.name=='sum') {
c.value = round((a.value*1) + (b.value*1));
} else if(e.target.name=='diff') {
c.value = a.value - b.value;
}
$.post("api.php/action", {a: a.value, b: b.value, c: c.value, action: e.target.getAttribute('name')}, function (data) {
console.log(data);
})
});
})();
Behat y Selenium
Behat es una herramienta para escribir pruebas de comportamiento automatizadas. Es la forma más natural para que los humanos prueben basándose en escenarios que pueden ocurrir al usar la aplicación. Selenium es un servidor que permite simular un navegador, equipado con una API de programación. Al combinar estas dos herramientas, ganamos la capacidad de escribir algo como un bot que visita nuestro sitio y realiza acciones específicas. Es el uso de esta herramienta que viste en el video al principio de la entrada.
Gracias al comando vendor/bin/behat --init
, behat genera un archivo por defecto features/bootstrap/FeatureContext.php
. Ampliaramos esta clase añadiendo MinkContext
. Esta es una colección de traducciones entre el lenguaje natural Gherkin
y las acciones realizadas por los controladores del navegador como selenium
.
Mencioné que Gherkin
es un lenguaje natural. En la documentación oficial, se presenta de la siguiente manera:
Gherkin es el lenguaje que Cucumber entiende. Es un Lenguaje Específico de Dominio, Legible por Negocios, que te permite describir el comportamiento del software sin detallar cómo se implementa ese comportamiento.
Además de esta extensión, añadiremos algunas funciones que faltan en MinkContext
.
<?php
use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use Behat\MinkExtension\Context\MinkContext;
/**
* Defines application features from the specific context.
*/
class FeatureContext extends MinkContext implements Context
{
/**
* Initializes context.
*
* Every scenario gets its own context instance.
* You can also pass arbitrary arguments to the
* context constructor through behat.yml.
*/
public function __construct()
{
}
/**
* @param String $field
* @param String $value
* @Given I set :field as :value
*/
public function iSetAs($field, $value)
{
$javascript = 'document.getElementById("'.$field.'").value='.$value;
$this->getSession()->executeScript($javascript);
}
/**
* @Then Result should be :value
*/
public function resultShouldBe($value)
{
$javascript = 'document.getElementById("c").value';
$realResult = $this->getSession()->evaluateScript($javascript);
if ( $value !== $realResult) {
throw new Exception(
"Actual result is:\n" . $realResult
);
}
}
/**
* @param String $number
* @When I wait :number ms
*/
public function iWaitMs($number)
{
$this->getSession()->wait($number);
}
/**
* @param String $number
* @When I wait :number ms for jQuery
*/
public function iWaitMsForJQuery($number)
{
$this->getSession()->wait($number, '(0 === jQuery.active)');
}
}
Estas funciones están configurando los valores del campo cuando no está en el formulario, comprobando la validez del resultado y esperando: normal, y permitiendo no esperar más tiempo si todas las solicitudes han sido ejecutadas.
Con el contexto preparado, podemos echar un vistazo al contenido del archivo que describe las pruebas
features/calculation.feature
Feature: Executing calculations on the website
In order to calculate sum or difference
As an web browser
I want to see result after pressing button
@javascript
Scenario Outline: Action on two numbers
Given I am on the homepage
And I set "a" as <a>
And I set "b" as <b>
When I press "<action>"
And I wait 1000 ms for jQuery
Then Result should be <result>
Examples:
| a | b | action | result |
| 1 | 2 | sum | 3 |
| 3 | 6 | sum | 9 |
| 100 | 2000 | sum | 2100 |
| -1.5 | -3.1 | sum | -4.6 |
| 1.9990 | -0.0090 | sum | 1.99 |
| 1 | 2 | diff | -1 |
| -1 | -2 | diff | 1 |
| 1.001 | 2.001 | diff | -1 |
| 0.993 | 9.33 | diff | -8.337 |
| 12 | -12 | diff | 24 |
Contiene un escenario que consiste en 6 pasos repetidos en 10 configuraciones. Estos pasos son cálculos típicos realizados en la página - establecer, a
, b
, seleccionar un botón, esperar el resultado y verificar su corrección.
Para que todo funcione correctamente, aún falta un archivo de configuración behat
. Es behat.yml
.
behat.yml
default:
extensions:
Behat\MinkExtension:
browser_name: chrome
base_url: 'http://localhost:9000'
sessions:
default:
goutte: ~
selenium:
selenium2: ~
Eso es todo. Si has seguido el código hasta este punto, conoces este proyecto al dedillo. Espero que hayas aprendido algo, y si ves áreas donde podría mejorar algo, no dudes en hacérmelo saber. Agradecería todos los comentarios constructivos.
Other articles
You can find interesting also.
Patrón pull-push de ZeroMQ para Node JS
El artículo enfatiza la flexibilidad de ZeroMQ para la mensajería en Node.js, destacando el patrón pull-push ideal para sistemas distribuidos de alto volumen.
Daniel Gustaw
• 4 min read
Ataque XSS utilizando estilo de script e imagen
Aprende a infectar una página usando un ataque XSS con las etiquetas script, style o image. Puedes ver cómo reemplazar el contenido de la página con el tuyo incluso sin javascript.
Daniel Gustaw
• 4 min read
Múltiplo Común Mínimo - Teoría de Números
Solución al problema "Arquería" de la sección "Teoría de Números" de "Hacker Earth". La tarea es determinar el mínimo común múltiplo de una secuencia de números.
Daniel Gustaw
• 4 min read