Thoughts on Software Reflexiones sobre cómo dearrollar software y no morir en el intento

Tienes que crear un framework

Siempre recomiendo utilizar cuantos más componentes ya disponibles mejor. Nunca construyas tu propio framework para:

  • Persistir datos
  • Mostrar información basándote en plantillas
  • Construir la navegación de un sitio web
  • Comunicar componentes remotos

Y tantos otros ejemplos que me vienen a la cabeza.

Sin embargo, hay un framework que siempre deberías construir en cualquier proyecto en el que vayas a participar. Es algo que deberías tener claro antes incluso de empezar. Cuanto antes lo construyas será mucho mejor y no hacerlo pone seriamente en riesgo tu proyecto.

Se trata de tu propio framework para automatizar tus pruebas. No es algo que puedas tomar de alguna otra parte, ni que puedas copiar y pegar de un proyecto pasado (aunque, evidentemente, haber construido uno en el pasado te ayudará a hacerlo ahora). Y no puedes por una sencilla razón: todavía no ha sido inventado. Y el único que puede inventarlo eres tú porque sólo tendrá sentido para ti.

Pero espera un momento entonces. Si sólo va a tener sentido para mí, ¿sigue siendo un framework? Sí y no, lo veremos más adelante. Ahora mejor empecemos con un ejemplo.

Registrando usuarios en una aplicación web

Imaginemos que hemos construido nuestra nueva aplicación web y, como sabemos que vamos a tener que iterar muchas veces sobre nuestro producto para dejarlo a punto, tenemos claro que necesitamos tantas pruebas automáticas como sea posible. De lo contrario, cada vez que cambiemos algo estaremos corriendo un serio riesgo de que el sistema falle (básicamente nos hemos leído algún libro del tipo The Lean Startup, algo sobre desarrollo ágil o, por lo menos, algún artículo en internet publicado en los últimos 10 años. Así que decidimos empezar creando una prueba para una parte fundamental de nuestra aplicación: el login de usuarios.

Como estamos empezando y todavía no sabemos muy bien cómo será el producto final, decidimos hacer una versión muy sencilla (ver ejemplo).

Login

http://cdpn.io/yqihp

Lo sé, es tan fea que casi hace que duelan los ojos. Pero tampoco Google tenía muy buena pinta cuando empezó y fíjate dónde está ahora. Así que no te preocupes, posiblemente cuando vayas por la iteración número 15 podrás pensar en dejarla bonita. De momento, nos sirve. El código HTML de nuestra página sería:

<html>
  <head>
    <title>Welcome back!</title>
  </head>
  <body>
    <h1>Please login to enjoy all of our shine new features</h1>
    <form action="/register.do">
      <p>Username <input type="text" id="user_name"/></p>
      <p>Password <input type="text" id="password"/></p>
      <p><input type="submit" value="Login"/></p>
    </form>
  </body>
</html>

Automatizando pruebas

Podemos crear una prueba automática que haga login en nuestra aplicación usando, por ejemplo, Selenium:

WebDriver driver = new HtmlUnitDriver();

driver.get("http://mydomain.com/login");

Webelement usernameInput = driver.findElement(By.Id("username"));

usernameInput.sendKeys("test-user");

Webelement passwordInput = driver.findElement(By.id("password"));

passwordInput.sendKeys("123456");

WebElement loginButton = driver.findElement(By.id("login"));

loginButton.click();

A partir de aquí, podríamos usar este mismo código en cualquier test que requiera el login del usuario y todo estaría bien… hasta que llegásemos al punto en el que queramos cambiar nuestra pantalla de login (iba a ser en nuestra iteración número 15, ¿no?) y tengamos que modificar cientos o, si hemos sido realmente buenos, miles de tests que se basaban en el código anterior.

Mejorando nuestras pruebas automáticas

El problema anterior básicamente tiene su origen en que hemos hecho nuestro código de prueba dependiente de algo que cambia de forma frecuente (especialmente en las etapas iniciales de un proyecto): la interfaz de usuario. Así que algo tan trivial como cambiar el identificador a los elementos HTML de la página de login puede hacer que cientos de tests empiecen a fallar.

La solución, como otras tantas veces, consiste en añadir un nivel de indirección que se encargue de manejar los detalles acerca de cómo está implementada la interfaz de usuario y, al mismo tiempo, ofrezca una interfaz consistente y homogénea con la que puedan interactuar todos los tests que construyamos. Así es como empezaremos a construir el framework que nos ayudará a automatizar nuestras pruebas.

En nuestro caso, aquellos tests que necesiten hacer un login de usuario se podrían escribir de la siguiente manera:

LoginPage.goTo();

LoginPage.setUsername("testuser");

LoginPage.setPassword("123456");

LoginPage.doLogin();

Una vez empecemos con la construcción de nuestro propio framework podemos ir más allá de la simple abstracción de la UI. Por ejemplo, podemos hacer cosas como:

  • Ofrecer un acceso rápido a usuarios ya creados para los que además conocemos determinados parámetros interesantes desde el punto de vista de las pruebas.
  • Implementar procesos largos que son repetidos por muchos tests.

En mi actual proyecto, por ejemplo, tenemos una serie de usuarios prototípicos que son utilizados repetidamente por toda nuestra suite de pruebas. Por ejemplo, Thomas Müller: como buen alemán medio que es, tiene contratado un seguro de responsabilidad civil (también tiene el nombre y el apellido más repetido en Alemania). Así que siempre que alguien necesita un usuario que tenga registrado un seguro de responsabilidad civil durante la construcción de una prueba sólo tiene que hacer:

TestUsersRegistry.getThomasMueller();

O si queremos crearlo desde cero:

TestUsersFactory.createThomasMueller();

O cuando es necesario registrar un usuario nuevo, sin contratos, para probar una funcionalidad disponible para usuarios recieén registrados (tenemos unas cuantas de ese tipo) sólo hace falta hacer:

TestUsersUtils.registerNewUser("John", "Doe", "john.doe@example.com");

En este caso, este método está sobrecargado con diferentes opciones para darnos la flexibilidad necesaria que nos permita registrar usuarios con todas las opciones que sean relevantes desde el punto de vista de los casos de prueba que estamos automatizando. Lo que nos lleva al siguiente punto importante cuando creas tu propio framework de automatización de pruebas: complejidad.

Características de un framework de automatización de pruebas

Por su propia naturaleza, el códido que escribimos cuando automatizamos pruebas tiene (o sería deseable que tuviera) las siguientes características:

  • Es bastante repetitivo. Muchas acciones se repiten a lo largo de múltiples tests (especialmente cuando implementamos diferentes escenarios para una misma funcionalidad)
  • Es lineal (o debería serlo; si no lo es entonces deberías revisar cómo estás implementando tus pruebas)
  • Es fácil de escribir (porque queremos que escribir tests sea algo sencillo que todo el mundo haga de forma regular)
  • Es fácil de leer. Esto es más importante aún porque: ** Queremos podemos entender más fácilmente qué esperamos de una funcionalidad con echarle un ojo al código que la prueba ** Cuando un test falla y queremos arreglarlo lo último que queremos es tener que perder un montón de tiempo averiguando qué es lo que el test estaba intentando hacer
  • Se aproxima lo máximo posible al lenguaje natural y utiliza cuantos más términos propios del dominio de nuestro sistema mejor. Esto es básicamente un corolario del punto anterior, pero quería resaltarlo para dejarlo bien claro.

Por todo esto, queda claro dónde tenemos que poner la complejidad: en nuestro framework de automatización de pruebas. Porque las cosas nunca son sencillas y, si queremos tener un código de prueba que sea super sencillo y fácil de entender, tiene que haber un sitio donde se encuentren todas las complejidades inherentes al código que estamos intentando crear.

Por eso, no es ningún problema que nuetro código de automatización de pruebas sea complicado. Tiene que serlo para que nuestras pruebas sean sencillas. La parte positiva es que será un código que escribirás una vez y lo reutilizarás miles de veces (el beneficio es claro).

Recuerda: no se trata de hacer las cosas más complicadas de lo necesario por el gusto de hacerlas complicadas. Pero no hay ningún problema si tu framework de automatización de pruebas empieza a serlo porque tu objetivo es hacer sencillas las miles de pruebas que quieres automatizar.

Esto nos lleva a las características deseables para nuestro framework de automatización de pruebas:

  • Permite escribir muchos tests con poco esfuerzo.
  • Ofrece interfaces claras que pueden ser utilzadas fácilmente por quienes tengan que escribir las pruebas automáticas.
  • Se acerca lo máximo posible al lengaje natural (lo que permite que las pruebas automáticas puedan ser escritas por más personas distintas a los propios desarrolladores del sistema).
  • Maneja todas las complejidades que puedan aparecer cuando tenemos que escribir el código de una prueba.
  • Es lo suficientemente flexible para facilitar la creación de pruebas para todos los escenarios que necesitemos (no sólo para los más sencillos).
  • Incorpora atajos que hagan la vida más cómoda a quienes escriben los tests. Por ejemplo, si hemos parametrizado el método que crea usuarios para que se puedan crear todos los tipos de usuarios con los que nos interesa probar, es perfectamente válida crear métodos adicionales que permitan crear determinados tipos de usuarios de forma fácil (sin tener que pasar ningún parámetro, por ejemplo) aunque eso pudiera considerarse redundante.

Al principio, crear tu framework de automatización de pruebas puede parecer una tarea complicada pero te aseguro que el beneficio es demasiado grande para ignorarla. Además, es parecido a ir al gimnasio: puede costar mucho al principio pero, cuando te acostumbras, no cuesta tanto y, finalmente, verás que lo haces con naturalidad.

Es también una excelente inversión porque, una vez hecho el esfuerzo inicial y puesto en marcha el framework, verás cómo cada vez inviertes menos tiempo en desarrollarlo y perfeccionarlo y, en cambio, pasas más tiempo usándolo y creando más y más pruebas automáticas lo que te ayuda a mejorar la calidad del sistema que estás construyendo (y, de paso, te hacer sentire mucho mejor como desarrollador y ser humano).

Comentarios finales

Aquellos que están familiarizados con temas como automatización de pruebas, TDD, etc. puede que se hayan sentido un poco confundidos por haber dejado en el aire algunas cuestiones importantes. Como, ¿para qué tipos de pruebas es importante crear nuestro framework? ¿Sólo para pruebas de aceptación? ¿Sólo cuando estamos probando a través de la UI, como en el ejemplo que hemos visto con Selenium?

Lo cierto es que he sido deliberadamente impreciso en este aspecto. En cualquier caso, debemos crear nuestro framework para cualquier tipo de prueba que automaticemos en nuestro proyecto, no importa que sean unitarias o integradas, de aceptación, de sistema… incluso para pruebas de humo.

Siempre que estemos repitiendo el mismo código una y otra vez cada vez que creamos un test y/o que dicho código dependa de cosas que van a cambiar con frecuencia estamos recibiendo una señal de que necesitamos refactorizar nuestro código de pruebas y empezar a crear nuestro propio framework. Es algo que debemos hacer a múltiples niveles y que tenemos que hacer con naturalidad (casi como respirar).

Referencias

Mi libro favorito sobre este tema, y con el que aprendí a escribir pruebas automáticas de una forma sostenible en el tiempo (y no el lío de código espagueti en el que solía convertir mis tests), es xUnit Test Patterns de G. Meszaros. Casi diría que es una lectura obligatoria (de la que ya hablé anteriormente aquí). Si no lo has leído, puedes empezar echándole un ojo al sitio web del libro, donde hay información muy útil.