lo que seguro
querés saber sobre
UNIT TESTING
en 60 minutos

(o casi todo)

(particularmente
para Front-End)

Joel A. Villarreal Bertoldi

Buenos Aires · Octubre 2016

¿QUÉ
SIGNIFICA
"TEST"?

"Un procedimiento cuyo objetivo es establecer la calidad, performance o confiabilidad de algo, especialmente antes de que sea conducido a un uso masivo."

¿Y UN TEST

UNITARIO?

"Un test unitario es una pieza automatizada de código que invoca a una unidad de trabajo en el sistema y luego chequea una única suposición acerca de su comportamiento"

"The Art of Unit Testing"
— Roy Osherove

"Una unidad de trabajo es un caso de uso lógico-funcional en el sistema que puede ser invocado por alguna interfaz pública. Puede abarcar un método, una clase o múltiples clases en conjunto para alcanzar un único objetivo verificable."

"The Art of Unit Testing"
— Roy Osherove

¿UNIDAD
DE TRABAJO?

 

Como siempre ocurre, hay múltiples herramientas para realizar este tipo de testing, especialmente en Front-End.

TEST DESCRIPTION FRAMEWORKS

CasperJS

Mocha

QUnit

Describen los tests.

Como siempre ocurre, hay múltiples herramientas para realizar este tipo de testing, especialmente en Front-End.

ASSERTION FRAMEWORKS

Jasmine

Assert

Chai

Describen las validaciones.

Vamos a trabajar con Mocha...
(como descriptor de tests en Karma*)

(*) Test runner

y con Chai.
(como assertion framework)

import { describe, it } from 'mocha';
import { expect } from 'chai';

import Stuff from './src/Stuff';

describe('Stuff', function() {
    it('should do something', function() {
        expect(new Stuff()).to.do('something');
    });
});

Descriptor del grupo de tests

Enunciado del test individual

Expectativa

¿Cómo es un test?

¿Cómo se escriben los tests?

Todo test debe responder

cinco preguntas esenciales.

(según un artículo de Eric Elliot) 

¿Qué estamos testeando?

¿Qué debería suceder?

¿Qué está retornando el test?

¿El test retorna lo que esperamos?

¿Cómo se puede reproducir el test?

VAMOS
PASO
A
PASO.

ESTE ES
NUESTRO
PROYECTO.

swissblade.js

Consta de:

tijera, lima, navaja,
abrelatas, sacacorchos, etc.

RESPONDAMOS LAS DOS
PRIMERAS PREGUNTAS.

Es importante no empezar por el código.

Es mucho más fácil comenzar
escribiendo en lenguaje natural
lo que se desea testear.

Tomemos el componente Scissor/Tijera.

Supongamos que existe un método:

SwissBlade.scissor.cut()


Que corta un papel.

SwissBlade.scissor.cut()


Debería dividir el papel en dos partes.

Debería cortar papel de hasta X espesor.

No debería poder cortar materiales duros.

import { describe, it } from 'mocha';
import { chai } from 'chai';

describe('Swissblade.scissor.cut', function() {
    it('should cut paper in two pieces');
    it('should cut paper of up-to a certain thickness');
    it('should not cut any hard materials');
});

Aquí describimos las partes a testear de nuestro componente Scissor. Nótese que no hemos definido ninguna función en it().

SINTAXIS DE UNA FRASE DE PRUEBA

it('should [not] <action> [condition]')

CONJUGACIÓN

NEGACIÓN

ACCIÓN

CONDICIÓN

Las pruebas se escriben como condicionales.

Pueden ser condiciones negativas.

Evalúan una
acción en
particular.

Pueden tener en
cuenta ciertas
circunstancias.

VEAMOS AHORA LA
3º Y 4º PREGUNTA:

¿CUÁL ES EL RETORNO?
¿Y EL VALOR ESPERADO?

import { describe, it } from 'mocha';
import { chai } from 'chai';

describe('Swissblade.scissor.cut', function() {
    it('should cut paper in two pieces');
    it('should cut paper of up-to a certain thickness');
    it('should not cut any hard materials');
});

Tomemos la primera prueba como base.

import { describe, it } from 'mocha';
import { chai } from 'chai';

describe('Swissblade.scissor.cut', function() {
    beforeEach(function() {
        this.scissor = new Swissblade.Scissor();
        this.paper = new Paper();
    });
    it('should cut paper in two pieces', function() {
        const actual = this.scissor.cut(this.paper);
        const expected = 2;
        expect(actual).to.be.an('Array');
        expect(actual.length).to.equal(expected);
    });
});

¿Qué tenemos aquí?

{

{

import { describe, it } from 'mocha';
import { chai } from 'chai';

describe('Swissblade.scissor.cut', function() {
    beforeEach(function() {
        this.scissor = new Swissblade.Scissor();
        this.paper = new Paper();
    });
    it('should cut paper in two pieces', function() {
        const actual = this.scissor.cut(this.paper);
        const expected = 2;
        expect(actual).to.be.an('Array');
        expect(actual.length).to.equal(expected);
    });
});

{

Un preinicializador.

Antes de cada prueba (de cada "it"), se ejecuta esta función.

import { describe, it } from 'mocha';
import { chai } from 'chai';

describe('Swissblade.scissor.cut', function() {
    beforeEach(function() {
        this.scissor = new Swissblade.Scissor();
        this.paper = new Paper();
    });
    it('should cut paper in two pieces', function() {
        const actual = this.scissor.cut(this.paper);
        const expected = 2;
        expect(actual).to.be.an('Array');
        expect(actual.length).to.equal(expected);
    });
});

{

Un comportamiento.

Definimos el comportamiento y tomamos el valor actual de la operación.

Definimos el valor esperado.

Escribimos las expectativas.

¿Y LA ÚLTIMA?
¿CÓMO SE
REPRODUCE EL TEST?

Toda operación que testeemos en nuestro código debe ser determinista.

A igual input, igual output

Por ende, el test se reproduce sabiendo sus condiciones de entrada y salida.

El preinicializador que vimos anteriormente es un tipo de hook provisto por Mocha.

before()
beforeEach()
after()
afterEach()

Se ejecuta una vez antes de la suite de test cases.

Se ejecuta una vez antes de cada test a correr.

Se ejecuta una vez después de la suite de test cases.

Se ejecuta una vez después de cada test a correr.

Hablemos de expectativas.

Una expectativa es un enunciado que describe el comportamiento esperado en un test case.

SINTAXIS DE UNA EXPECTATIVA

expect(<subject>).to.<predicate>[(value)]

SINTAXIS DE UNA ASEVERACIÓN (*)

assert(<expression>[, message])

(*) Otra forma de validar comportamientos.

expect(this.day).to.equal('Awesome!')
assert(this.day === 'Awesome!', 'Oh :(')
to be been is that
which and has have with
at of same

CONECTORES

expect(this.day).to.have.been.called()

PREDICADOS: true, false, ok, not

expect(this.story).to.be.true;
expect(this.euro).to.not.be.false;
=> this.story === true
=> !(this.euro === false)
expect(this.day).to.be.ok;
=> !!this.day

PREDICADOS: null, undefined, instanceof

expect(this.contract).to.be.null;
expect(this.feeling).to.not.be.undefined;
=> this.contract === null
=> !(this.euro === undefined)
expect(this).to.be.an.instanceof(Freedom)
=> this instanceof Freedom === true

http://chaijs.com/api/bdd/

EL RESTO DEL DICCIONARIO

npm install --save-dev chai-backbone

EXTRA-ESPECIAL PARA CHAI

Agrega predicados específicos para Backbone.

expect(model).to.trigger('change').when(callback)
expect('/my/way').to.route.to(Router, method[, {params}])
expect(view).to.call('method').when(callback)

Testear es una actividad
importante y aún subestimada.

(En todo el universo.)

REGLAS

Legibilidad

Mantenibilidad

Confiabilidad

REGLAS DE LEGIBILIDAD

No abusar de los preinicializadores

Testear sólo una cosa

Dar buenos nombres a los tests

Nada de magic strings o magic numbers

REGLAS DE MANTENIBILIDAD

Tests aislados y repetibles

Testear sólo API pública

No sobreespecificar los tests

State-based over Interaction-based

REGLAS DE CONFIABILIDAD

Nada de lógica o valores dinámicos

No mezclar con tests de integración

Usar siempre valores fijos (no new Date)

DEMOS/EJEMPLOS/REAL LIFE

¿Preguntas?

¡Gracias y buenas tardes!