"La duplicidad es m谩s barata que la mala abstracci贸n"

-- 鉁嶏笍 Sandi Metz

Hemos visto c贸mo estructurar una prueba de comportamiento con un t铆pico Hola Mundo razonable. Ahora vamos a prestar atenci贸n al c贸digo de las pruebas para saltarnos lo menos posible los principios del c贸digo limpio.

Se dice que el c贸digo de las pruebas debe ser muy sencillo, que cualquiera lo entienda a primera vista. Que no debe abusar de indirecciones, abstracciones o cualquier artificio que dificulte su entendimiento.

Estoy totalmente de acuerdo. Pero. Que nadie interprete esto como una barra libre para saltarse cualquier norma de limpieza de c贸digo y construir una pocilga en la que chapotear. Las pruebas son c贸digo, y el c贸digo ha de estar limpio.

Aunque, efectivamente, las pruebas son un tipo de c贸digo muy concreto que tiene sus particularidades. Podr铆amos permitirles ciertas licencias. Pero otras rotundamente no:

Malos olores en las pruebas

Comentarios

En las pruebas el texto de salida es muy importante; no es un adorno, es parte de la soluci贸n. Debemos aprovechar ese mismo texto para aclarar en c贸digo la intenci贸n de la prueba. No hacen falta m谩s comentarios.

Excepcionalmente se admiten encabezados que definan las funcionalidades siguiendo alg煤n patr贸n formal, tipo historia de usuario.

Datos m谩gicos

Es habitual compartir datos de entrada en varias pruebas. 驴Por qu茅 no van a estar en sus propias variables? 驴Por qu茅 los resultados esperados no van estar en sus variables? 驴Por qu茅 hay cadenas y n煤meros desperdigados por las pruebas?

Datos absurdos

Si una prueba necesita un email de entrada 驴Qu茅 ganas aporreando el teclado y generando sdfasd@yjtj.cf ?

Anidamientos sin fin

La propia sintaxis de las pruebas nos lleva a encadenar funciones de distintos niveles: 驴Te gusta esto?

describe('',()=>{context('',()=>it('', ()=>{yourTestingCode(yourParam);}))})

Recomiendo encarecidamente que se extraigan todos los callbacks a funciones con nombre. Es c贸digo, hay que depurarlo, puede reutilizarse. Repito; es c贸digo, tr谩talo con respeto.

Licencias para manchar

DRY, WET y DAMP

DRY

El principio DRY (Don麓t Repeat Yourself) asegura que "La duplicidad es el principal enemigo de un sistema bien dise帽ado". Pero no puedes combatirla a cualquier precio. Normalmente se requieren mecanismos de abstracci贸n que obligan a un esfuerzo cognitivo extra. No queremos eso en las pruebas. Por tres buenas razones.

  • Primero porque las pruebas deben ser f谩cilmente entendidas y modificadas por cualquiera.
  • Segundo porque el c贸digo de una prueba debe aislarse de las dem谩s; no debe tener efectos secundarios, y por tanto la duplicidad es menos problem谩tica.
  • Por 煤ltimo y fundamental: las pruebas no tienen pruebas. Si algo falla haz que sea evidente.

WET

Por contra tampoco es pr谩ctico caer en el WET (Write Everything Twice). Incluso afecta a la moral del equipo verse repitiendo, copiando y pegando, siempre el mismo c贸digo. Hay que buscar un equilibrio.

DAMP

Para completar el juego de palabras tenemos otro acr贸nimo aplicable: DAMP (Descriptive And Meaningful Phrases). Se refiere a que el c贸digo es m谩s entendible cuanto mejor nombrado est茅. Algo que cumplen los frameworks de testing facilitando las aserciones y que cumplimos los programadores al rellenar las cadenas de cada describe('') y de cada it('')

Recomendaciones para el c贸digo de pruebas

Habr谩s adivinado que ni un extremo ni el otro. Considerando todo lo anterior, las recomendaciones se resumen en:

  • Muchas pruebas peque帽as.
  • Un fichero, m贸dulo, para cada prueba.
  • Textos super mega ultra hyper descriptivos.
  • Extrae los datos a variables o constantes del m贸dulo.
  • Extrae los callbacks a funciones locales con nombre AAA.
  • Perm铆tete peque帽os ficheros de utilidad comunes a las pruebas.
  • Pero sin abstracciones complejas, s贸lo configuraciones o funciones.

Ejemplo con Cypress

En el laboratorio te muestro un para de t茅cnicas que pueden fomentar la legibilidad de las pruebas, y al mismo tiempo evitar ciertas redundancias.

Intenci贸n, comentarios y otras historias

Hay frameworks que transforman el comentario en c贸digo, siguiendo normas de leguajes formales. Est谩 fuera del alcance de este tutorial. Yo os propongo que al menos encabec茅is las pruebas con una declaraci贸n de intenciones:

// FEATURE:     the app should have a well formed html// As a:        user// I want to:   view a recognizable web page// In order to: feel safe using it

Textos significativos y descriptivos

Si quieres entender algo de un vistazo, mejor que sea conciso:

describe(`GIVEN: the proton tasks web app`, () => {
  arrangeTest();
  context(`WHEN: I visit the url ${Cypress.env(baseUrl)} `, () => {
    actVisit();
    it(`THEN: should have charset UTF-8`, assertCharset);
    it(`AND THEN: should have _Proton Tasks_ on Title`, assertTitle);
    it(`AND THEN: should have a header`, assertHeader);
    it(`AND THEN: should have an h1 on the header with text _Proton Tasks_`, assertH1ContainText);
  });
});

En el anterior ejemplo se aplican algunas de las pr谩cticas recomendadas. Poco anidamiento, c贸digo en funciones con nombre y textos descriptivos siguiendo un patr贸n.

Variables y funciones

Nunca viene mal empezar con una secci贸n para declarar variables o constantes.

let sutUrl;
let expectedTitle;
let selectorHeader;
let selectorH1;

La asignaci贸n de valores ir谩 en alguno de los m茅todos de preparaci贸n

function arrangeTest() {
  ignoreParcelError();
  sutUrl = Cypress.env('baseUrl');
  expectedTitle = 'Proton Tasks';
  selectorHeader = 'header';
  selectorH1 = 'header > h1';
}

Y hablando de las funciones, y recordando a la Triple A, te propongo que todas empiecen por uno de esos tres verbos Arrange-Act-Assert. Como en el siguiente ejemplo.

function arrangeTest() {
  ignoreParcelError();
  sutUrl = Cypress.env('baseUrl');
  expectedTitle = 'Proton Tasks';
  selectorHeader = 'header';
  selectorH1 = 'header > h1';
}

function actVisit() {
  before(() => cy.visit(sutUrl));
}
function assertCharset() {
  cy.document().should('have.property', 'charset').and('eq', 'UTF-8');
}

Configuraciones, datos, utilidades y ejecuci贸n

Y ya est谩; s贸lo nos quedar铆a extraer algunas configuraciones y utilidades que se reutilicen entre pruebas.

Configuraci贸n

En Cypress nos ofrecen un fichero de configuraci贸n: el cypress.json. Aqu铆 va la configuraci贸n normal de trabajo del framework. Pero adem谩s puedes usarlo para tus propias variables.

Incluso puedes sobrescribir en tu m谩quina local con valores espec铆ficos en un fichero llamado cypress.env.json que no debes subir al repositorio remoto ni a producci贸n.

La idea es que puedas adaptar f谩cilmente los tests a distintos entornos. Teniendo una configuraci贸n para integraci贸n

{
  "chromeWebSecurity": false,
  "baseUrl": "https://labsademy.github.io/ProtonTasks/",
  "env": {},
  "video": true
}

Y otra para desarrollo

{
  "chromeWebSecurity": false,
  "baseUrl": "http://localhost:1234",
  "env": {},
  "video": false
}

En cualquier caso puedes acceder a la configuraci贸n usando el comando Cypress.env(nombrePropiedad). Pero esto est谩 pensado para configuraciones, no para datos.

Datos enviados y esperados

En Cypres tambi茅n tienen su lugar y su nombre: fixtures. Se trata de que para lleves a ciertos fichero los datos que vas a usar como par谩metros de entrada o como muestras esperadas.

Es clave usar este tipo de convenios para no sobrecargar el cerebro del siguiente lector. Todo debe ser claro y evidente para todos.

Comandos

Llegamos a un asunto peliagudo. Compartir o reutilizar l贸gica y utilidades. Cypress nos ofrece su propia soluci贸n y le llama commands. En principio est谩 bien y es sencilla de usar.

Cypress.Commands.add('typeText', (selector, text) => cy.get(selector).type(text));

Se trata de ense帽arle nuevos trucos a un perro viejo. El problema es que es un mecanismo din谩mico; y si queremos que los usen hay que sab茅rselo de memoria o liarse m谩s para ofrecer intellisense.

 cy.typeText(selector, text);

La alternativa es usar un funci贸n de toda la vida y exportarla.

export const typeText = (selector, text) => cy.get(selector).type(text);

Y luego importarla y usarla como si tal cosa.

import { typeText } from '../../support/actions';typeText(selector, text);

Cualquiera de las dos funciones es v谩lida y te puede ayudar a reducir c贸digo repetitivo. Especialmente para preparar y limpiar escenarios antes y despu茅s de los tests. Por ejemplo, haciendo login, logout, creando o borrando datos...

Scripts

Por 煤ltimo, queda decir que la ejecuci贸n de las pruebas puede hacerse presencialmente o de manera desatendida.

En el primer caso el desarrollador ejecuta las pruebas mientras desarrolla, o antes, si es un TDD practitioner. Esta es una gran ventaja de Cypress, que es r谩pido y agradable de usar en el d铆a a d铆a. En ese caso yo lo arranco con el navegador abierto.

Es igualmente interesante lanzarlo en modo consola, incluso desatendido o como parte de alg煤n sistema de integraci贸n continua. Aqu铆 ya entrar铆a m谩s a configura cosas como si graba o no los videos, las fotos y el formato del reporte.

En cualquier caso, escriptarlo es as铆 de f谩cil, y la configuraci贸n a su fichero...

  "scripts": {"start": "cypress open","test": "cypress run"}

En definitiva, las pruebas son c贸digo y merecen un respeto. Cypress te ofrece lo necesario para que sean f谩ciles de entender y mantener.