14/02/2020 | Consejos tecnológicos,Desarrollo de aplicaciones,Desarrollo de software

Pruebas extremo a extremo en Angular con Cucumber, Protractor y TypeScript

En la anterior y completa entrada sobre pruebas extremo a extremo (E2E) trabajamos con Angular, Cucumber, Protractor y JavaScript. Como sabemos, Angular 2 usa TypeScript, con lo cual veremos un ejemplo de cómo se haría con ese lenguaje. Tendremos preparado todo el tooling tal y como fue explicado en el post anterior, de forma que hoy nos centraremos en desarrollar e implementar pruebas con TypeScript. Usaremos de ejemplo una app básica creada a partir del «starter» de Angular y alojado en https://stackblitz.com/edit/angular-uqju6b-p8h1rb con la siguiente estructura de los tests (directorio “/tests”):

La aplicación consiste en una tienda online con un catálogo de productos, los cuales puedes ver en detalles, compartirlos, suscribirse a notificaciones de bajada de precio y comprarlos.

Haremos dos pruebas: una para comprobar que podemos acceder a los detalles de un producto y otra para ver que la compra funciona.

También señalaremos algunas conclusiones que hemos aprendido a partir de nuestra experiencia haciendo testing en Tribalyte.

Prueba 1: acceso a detalles

En “/catalog” encontramos la lista de productos. Cuando el usuario pulsa en el nombre de uno de los productos de la lista, este accede a sus detalles en una nueva pantalla con una URL, o ruta, distinta: “products/:productId”, donde “productId” es una ID interna que identifica al producto.

Intentaremos acceder tanto a los productos con detalles disponibles como a los que no, es decir, tendremos una feature (llamada details.feature) con dos scenarios. En Gherkin sería algo como:

Feature: Click on a product's name and access its details

Scenario: I click on a product that has a description available
Given I go to the "catalog" page
When I click on the first product name
Then I should see these specific details
| type |
| name |
| price |
| description |

Scenario: I click on a product that does not have a description available
Given I go to the "catalog" page
When I click on the last product name
Then I should see these specific details
| type |
| name |
| price |


Cada paso, como “Given I go to the «catalog» page”, tiene una implementación por detrás en TypeScript que es la que de verdad entra a nuestra aplicación, simula ser un usuario navegando de un sitio a otro, pulsando botones, rellenando formularios, haciendo scroll, etc. ¿Dónde encontramos estas implementaciones? Pues nos vamos al directorio “/steps” y creamos un nuevo archivo con un nombre igual al de la «feature» pero con otra extensión: details.steps.ts.

Observamos algo especial la descripción del «step»: hay una palabra entre comillas. Las dobles comillas van a ser como un comodín que contendrá cualquier valor que nosotros queramos, gracias a la captura mediante expresiones regulares. Esto hará que nuestra implementación sea reutilizable, como podemos ver en la implementación:

Given(/^I go to the "([^"]*)" page$/, async (url: string) => {
    await commonPage.goTo(url);
});

Al igual que en la «feature», empieza por la palabra reservada “Given” seguida del predicado de la step, con una expresión regular que contendrá lo que habíamos puesto entre comillas en la «feature», la URL. Dentro de la función, Protractor usará el objeto “browser” para navegar hasta la url que le hemos pasado. Cuidado con no olvidar los “awaits”, es importante gestionar bien las llamadas asíncronas.

    public async goTo(url: string): Promise<any> {
        await browser.get(url);
    }

Es útil utilizar Page Objects para mantener organizados nuestros tests a través de la encapsulación de elementos de nuestra aplicación. Aquí, tendremos funciones que solo se utilizarán en un pantalla en concreto de nuestra aplicación o incluso tener un Page Object con funciones en común a todas las pantallas.

Este último caso es el de common.po.ts con funciones para rellenar un input, hacer click en algo, hacer scroll, etc., que son acciones que probablemente se usen en más de una pantalla:

public async scrollAndClick(el: ElementFinder): Promise {
  await this.scrollToElement(el);
  await browser.wait(ExpConds.visibilityOf(el));
  await browser.wait(ExpConds.elementToBeClickable(el));
  await el.click();
}

public async scrollToElement(el: ElementFinder): Promise {
  await browser.executeScript("arguments[0].scrollIntoView({behavior: \"auto\", block: \"center\", inline: \"nearest\"})", await el.getWebElement());
}

Para buscar elementos en el DOM, podemos usar distintos tipos de selectores. Por ejemplo, para obtener el primer elemento de la lista de productos, lo seleccionamos mediante un selector CSS:

public getFirstProductName(): ElementFinder {
  return element(by.css("app-product-list > .product:first-of-type .name"));
}

Ahora ejecutamos los scripts con los siguientes comandos en orden:

npm run e2e:webdriver-update
npm run e2e:protractor

Prueba 2: compra

Feature: Purchase a product

@duck
Scenario: I click on a product and purchase it
Given I go to the "catalog" page
When I click on the first product name
And I click the buy button
And I click the checkout button
And I should see the "Phone XL" product
And I fill in the form
| name | type | data |
| name | input | Elena Robles |
| address | input | Avda. Andalucía, 73 |
And I click the purchase button
Then I should see "Purchase successful!" message

Algo muy útil son las tablas, como podemos ver en la step “I fill in the form”. Nos permiten pasar conjuntos de datos que se comportan como objetos JavaScript. Es como cuando usábamos las dobles comillas en medio de la descripción de una step, pero de otra forma y permitiéndonos mantener de una manera organizada mejor los datos.

Por otra parte, también podemos usar templates de ES6 para reusar funciones. Aquí, buscará diferentes “inputs” dependiendo del formControlName que le pasemos:

public getInput(name: string): ElementFinder {
  return element(by.css(`input[formControlName=${name}]`));
}

Si ejecutamos el test, vemos que todo va bien:

Si nos fijamos en la feature, vemos que justo encima del escenario hay una expresión especial. Son las llamadas tags, que para ejecutar solo los escenarios que queramos. Podemos darle cualquier nombre, en este caso, añadimos “@duck” a la configuración de Protractor y ejecutamos normalmente.

...
cucumberOpts: {
  require: [
    './steps/*.steps.ts'
  ],
  strict: true,
  'fail-fast': true,
  tags: "@duck"
},
...

Conclusiones aprendidas a partir de nuestra experiencia.

Cuidado de no olvidar los awaits. Una mala gestión de la asincronía puede provocar el famoso error de timeout “Failed: Timed out waiting for asynchronous Angular tasks to finish after X seconds. This may be because the current page is not an Angular application”. Las funciones que implementemos siempre tendrán que devolver algo, por ejemplo, promesas. Si esta no está correctamente gestionada y devuelta, Protractor se quedará esperando y “colgado”, dando este error.
Para poder usar «await», hay que añadir a la configuración de Protractor:

SELENIUM_PROMISE_MANAGER: false

También puede suceder que aunque usemos un selector CSS válido, el elemento todavía no esté presente en el DOM, como cuando un *ngIf está protegiendo un nodo del DOM hasta que recibe datos de una petición al servidor y los dibuja. Si Protractor no lo puede encontrar, se queda colgado. Para ello, debemos esperar a que el elemento sea visible o pulsable:

  await browser.wait(ExpConds.visibilityOf(el));
  await browser.wait(ExpConds.elementToBeClickable(el));

Podemos usar condicionales. Por ejemplo, para esperar a que un toast haya desaparecido:

if (toast && await toast.isPresent()) {
    await browser.wait(ExpConds.invisibilityOf(toast));
}

// Continue doing things

Como usuarios reales, no podemos pulsar un botón en una página si no lo vemos, es decir, tenemos que hacer scroll hasta él antes de pulsarlo.Para ello implementamos la función

scrollAndClick()

A veces, ciertas animaciones de Ionic hacen que haya elementos que no sean seleccionables/usables por Protractor de forma instantánea. Por ejemplo, si tenemos un side menu con una animación cuando se abre, puede que necesitemos esperar unos milisegundos a que lo haga antes de controlarlo mediante Protractor. Igualmente, podemos usar:

  await browser.wait(ExpConds.visibilityOf(sideMenu));

Las animaciones de Ionic pueden dar problemas en máquinas con pocos recursos. Si lo necesitas, puedes desactivarlas opcionalmente capturando un parámetro de la URL como.

public async navigateTo(url: string): Promise {
  return await browser.get(url + "?animations=false");
}

Y luego desactivando las animaciones con:

const isAnimated = window.location.search.indexOf("animations=false") < 0;
ionicConfig.set("animated", isAnimated);
 

Esperamos que esto os pueda ser de utilidad. ¡Hasta la próxima!


Compartir en:

Relacionados