martes, 19 de septiembre de 2017

Test EndToEnd de formularios con Protractor

Las pruebas "EndToEnd" quiere decir que hagamos las pruebas de inicio a fin como su nombre indica. Dicho de otra manera, es una técnica que se utiliza para comprobar si el flujo de una aplicación se comporta como se esperaba.

Las pruebas EndToEnd son de gran utilidad ya que garantiza que la aplicación hace lo que tiene que hacer, y además ayuda mucho a automatizar tareas que de otra forma llevaría mucho tiempo si se realiza de forma manual.

La herramienta con la que vamos a realizar este tipo de pruebas (y que viene por defecto en Angular-cli) se llama Protractor, y nos permite programar la interacción con el navegador como si fuésemos un usuario.

Imaginemos un formulario que tiene bastantes campos y tenemos que testear su comportamiento. Realizar esto de manera manual lleva tiempo y es tedioso, y en este punto (y en otros muchos) es donde Protractor nos puede ahorrar mucho tiempo...y es lo que vamos a ver en la entrada de hoy. Vamos a crear un típico formulario de un pedido de un producto y veremos como hacer el testeo del mismo.

Por simplificar vamos a crear un formulario con 4 campos. En una situación "real" con muchos más campos el ahorro de esfuerzo al usar Protractor sería mucho mayor. En cualquier caso haciendo pruebas end to end nos aseguramos de que el componente se comporta como se espera.

Nuestro app.component es muy sencillo (he obviado temas de css, y no he usado reactiveForms por centrar la atención en las pruebas) y tiene el siguiente aspecto:

import { Component } from '@angular/core';

@Component({
selector: 'app-root',
template: `
<h3>Formulario de pedido</h3>
Producto: <select id="producto" [(ngModel)]="producto">
<option value="">Seleccionar</option>
<option value="1">Producto-1</option>
<option value="2">Producto-2</option>
<option value="3">Producto-3</option>
</select><br>
Cantidad: <input type="text" id="cantidad" [(ngModel)]="cantidad"><br>
Email: <input type="email" id="email" [(ngModel)]="email">
<input type="checkbox" [(ngModel)]="chkTerminos" id="chkTerminos"> He leido los términos y condiciones<br>
<button [disabled]="!chkTerminos" id="btnSend">Solicitar producto</button>
`
})
export class AppComponent {
chkTerminos: false;
producto = '';
cantidad: number = null;
email = '';
}

Hay un botón de "Solicitar producto" que se habilita únicamente si se ha marcado el checkbox de "He leido las condiciones...."
En una situación "real" no habilitaríamos el botón si el email no es válido, si la cantidad no es válida o si no se ha seleccionado un producto, pero para hacernos una idea de uso, vamos a chequear únicamente que se haya marcado el checkbox.

La carpeta donde se encuentran las pruebas que angular nos crea por defecto, y donde dejaremos las nuestras es la carpeta e2e.
Dentro de esta carpeta nos encontramos el fichero app.e2e-spec.ts y el fichero app.po.ts además del fichero de configuración de Protractor.

Si abrimos el fichero app.e2e-spec.ts veremos que tiene una sintaxis similar al de las pruebas unitarias con karma-jasmine. Tiene un "describe" que es un agrupador de pruebas sobre algo concreto (en las pruebas end-to-end normalmente es un describe por página) y dentro del describe es donde meteremos los "it" que queremos testar.

Si ejecutamos las pruebas con el comando ng e2e veremos que fallan (se abrirá el navegador, hace una interacción muy rápida y se vuelve a cerrar mostrando el resultado en la terminal). Y fallan porque la prueba que nos trae por defecto es la siguiente:

it('should display message saying app works', () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual('app works!');
});

y nosotros hemos quitado el mensaje de "app works" que es lo que la prueba espera obtener.
En el código anterior, la instancia de la pagina llama al método navigateTo() que esta dentro del fichero app.po.ts como veremos posteriormente. Una vez que la página se ha dirigido a la ruta establecida en dicho metodo, llama al método getParagraphText() que obtiene un elemento cuyo selector es especificado y devuelve el texto de dicho elemento, y comprueba (espera) que el texto devuelto sea igual a "app works!"

Vamos a crear una prueba a modo de ejemplo que nos indique que el título de la página se muestra correctamente, pero antes de eso vamos a explicar brevemente algunos cosas:

Junto al fichero app.e2e-spec.ts se encuentra el fichero app.po.ts. Este fichero (tendremos un fichero .po.ts por cada página que queremos testear) contiene los métodos mencionados anteriormente y todos aquellos métodos que nosotros queramos añadir para nuestras validaciones.

En nuestro ejemplo el fichero app.po.ts, (correspondiente al app.component) tiene el siguiente aspecto:

import { browser, element, by } from 'protractor';

export class HomePage {
navigateTo() {
return browser.get('/');
}

getParagraphText() {
return element(by.css('h3')).getText();
}
}

Como hemos comentado inicialmente consta de dos métodos. uno para "navegar" a la url especificada (en nuestro ejemplo a la ruta raíz) y otro que viene por defecto que es el getParagraphText que devuelve el texto del selector css especificado, es decir, en este ejemplo el "h3" que contiene el título del formulario.

Si ahora en la página app.e2e-spec.ts creamos un método "it" como el que sigue a continuación (y eliminamos el que había anteriormente ya que fallaba) y lanzamos el test de nuevo (ng e2e) debería devolver un resultado satisfactorio.

it('should display form title', () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual('Formulario de pedido');
});

Hasta aquí únicamente hemos comprobado que la página contiene un título que es el que nosotros esperamos. Vamos a ver ahora como podemos validar el formulario.

En primer lugar crearemos un método como el siguiente:

submitForm() {
element(by.cssContainingText('option', 'Producto-1')).click();
element(by.css('input[id="cantidad"]')).sendKeys('2');
element(by.css('input[id="email"]')).sendKeys('jorgetriguerosfalque@gmail.com');
element(by.css('[id="chkTerminos"]')).click();
element(by.css('[id="btnSend"]')).click();
}

Este método selecciona la opción del despegable "Producto-1" y hace clic sobre ella.
Selecciona el elemento input cuyo id="cantidad" y escribe sobre él el número 2
Selecciona el elemento email cuyo id="email" y escribe sobre él un email.
Selecciona el elemento cuyo id="chkTerminos" y hace clic sobre el, es decir, marca el checkbox, y por último, hacemos click en el botón (esto es posible porque previamente hemos seleccionado el checkbox, si no no estaría habilitado)

Sólo nos queda crear el "it" en la página app.e2e-spec.ts:

it('should Show button when checkbox is checked', () => {
page.navigateTo();
page.submitForm();
});

Si lanzamos las pruebas (ng e2e) veremos como se levanta una instancia del navegador y se cumplimenta el formulario con los datos que hemos establecido y se procede a la pulsación del botón de envío.

En el siguiente pantallazo se observa como las pruebas han sido satisfactorias.



Código fuente disponible: https://github.com/jorgetrigueros/ng4-protractor-forms-test

martes, 5 de septiembre de 2017

Lazy loading

Un módulo en angular es un conjunto de componentes, servicios, directivas, etc. En su modo más simple un módulo contiene al menos un componente.
En angular por defecto existe un módulo inicial que se llama AppModule que inicialmente carga todos los módulos necesarios y todos los componentes de la aplicación.
Esto no es muy útil, porque a fin de cuentas los módulos de angular pueden ser fragmentos más pequeños sin necesidad de tener que cargar todos inicialmente aunque el usuario no los vaya a usar.
Por ejemplo, si nuestra aplicación tiene 80 componentes podemos cargar todos en un único módulo, sin embargo estaríamos rompiendo el principio de "divide y vencerás".
Así por poner un ejemplo, nuestra aplicación de 80 componentes quizás podría dividirse en 10 módulos de 8 componentes cada una, y esto supone varias ventajas, por un lado dividir los componentes en unidades que tienen una funcionalidad especifica y es más fácil de gestionar, y por otra parte, los módulos en Angular es el requisito indispensable para usar lo que se conoce como
"carga perezosa" o "lazy loading", que es lo que vamos a ver en esta entrada.

Con la carga perezosa nuestra aplicación no necesita cargar todo a la vez, sólo necesita cargar lo que el usuario espera ver cuando la aplicación se carga por primera vez. Los módulos que se cargan perezosamente sólo se cargarán cuando el usuario navega por sus rutas. Y esto lógicamente nos ayuda a reducir el tiempo de arranque de la aplicación.

Vamos a verlo con un ejemplo.

Supongamos una aplicación que tiene un menú con una serie de opciones (Clientes, proveedores y usuarios). Cada una de estas opciones cargará su módulo correspondiente, pero lo cargará únicamente cuando el usuario acceda a dicha opción.

El menú de la aplicación tendrá el siguiente aspecto:



Manos a la obra...

En primer lugar, vamos a crear el componente menu (menu.component) y los componentes de Clientes (cliente-list.component) y Proveedores (proveedores-list.component)

El componente se cargará desde el modulo principal, pero los componentes de clientes y proveedores se cargarán bajo demanda por "lazy loading".

El código del componente menú es el que vemos a continuación.



Es un menú creado con el framework css bulma, y realmente no hay nada especial que comentar.

Vamos a mostrar el código del componente cliente-list (el componente proveedor-list es muy similar así que no lo pondré en esta explicación, aunque si estará en el código fuente disponible)



El componente anterior es muy sencillo. Carga una serie de clientes, y finalmente muestra un botón para crear un nuevo cliente.

Hemos comentado anteriormente que la base para poder hacer "lazy loading" son los módulos, así que vamos a crear un módulo para Clientes. En realidad serán dos módulos, uno para los clientes (clientes.module.ts), y otro para gestionar las rutas de clientes (clientes-routing.module.ts)

clientes.module.ts



En este módulo de clientes importamos el clientes.routing.module que veremos a continuación y declaramos dos componentes el ClienteListComponent que teniamos creado anteriormente, y el NuevoClienteComponent.ts que crearemos posteriormente a modo de ejemplo.

Dentro del clientes-routing.module.ts es donde definiremos las rutas de clientes:


Las rutas que hemos definido son la principal, que cargará el componente ClienteList, y una para crear un nuevo cliente que cargará el:

nuevo-cliente.component.ts



Deberíamos hacer lo mismo para "Proveedores" y "Usuarios" pero por motivos de espacio no vamos a mostrarlo aquí (aunque si vendrá en el código fuente disponible)

A llegado la hora de hacer el lazy loading cuando el usuario haga click en la opción de menú "Clientes".

Para indicar que una ruta queremos cargarla de forma "perezosa" tenemos que indicarlo con este formato:

  {
    path: 'clientes',
    loadChildren: './clientes/clientes.module#ClientesModule'
  }

Si nos fijamos en la propiedad loadChildren, tenemos que espeficicar donde se encuentra la ruta física relativa del fichero clientes.module (sin extensión), junto con el símbolo # (almohadilla) y el nombre de la clase del módulo.

Luego en el fichero principal de rutas, declaramos las rutas, y especificamos cuales queremos que se carguen de forma "perezosa"



Cuando cargamos nuestra aplicación, inicialmente veremos que se cargan los siguientes ficheros (esto podemos comprobarlo si desde Chrome accedemos a las herramientas del desarrollador, pulsando F12, y desde ahí en la pestaña Network)


Los ficheros que se muestran anteriormente son los polyfills, styles, vendor, etc.

Sin embargo, cuando hacemos click en la opción de clientes nos aparece un nuevo fichero que se llama 0.chunck.js (el indice lógicamente puede variar) 


Este fichero es el que contiene el código correspondiente a la parte de clientes, que es la que hemos cargado "bajo demanda", es decir, que no se ha cargado inicialmente desde el principio, lo que nos sirve para optimizar los tiempos de carga iniciales de nuestra app.

Puedes descargar el código fuente del proyecto en:

https://github.com/jorgetrigueros/ng4-lazy-loading

Espero que esta entrada le sirva para saber que es el "lazy loading" y como podemos usarlo en nuestros proyectos.

Hasta la próxima entrada :-)