Node.js

Testing de serveis REST

  

En documents anteriors hem utilitzat eines com EchoAPI o similars per a provar els nostres servicis REST. Estes eines resulten útils per a proves puntuals de servicis, i comprovar que, en principi, complixen la seua comesa. Algunes, com Postman, permeten fins i tot definir bateries de proves que llançar sobre els nostres servicis i detectar si es passen totes. Però, en qualsevol cas, el procés pot resultar una mica pesat, i en alguns casos limitat a un cert nombre de proves gratuïtes.

En este document veurem com automatitzar les proves sobre els nostres servicis REST de dos formes possibles:

1. Testing en local des de JavaScript: Axios

Un dels principals inconvenients que podem trobar a l’hora de provar les nostres aplicacions REST amb eines com EchoAPI o Postman és que el seu ús gratuït té unes certes limitacions, bé perquè no deixen fer el mateix que altres aplicacions comercials, o pel nombre de vegades que podem executar una col·lecció de manera gratuïta en algunes d’elles.

Per a pal·liar esta deficiència i complementar millor les nostres proves podem fer ús d’alguna llibreria específica en JavaScript que permeta llançar-les de manera gratuïta i il·limitada des del codi. És el cas d’Axios.

1.1. Instal·lació i configuració

Les proves que fem en Axios les podem definir en un projecte a part del qual vulguem provar, o bé podem definir alguna carpeta tests dins del propi projecte. Encara que, això sí, necessitarem executar el programa de tests des d’un terminal, i pot ser que el terminal del projecte principal ja estiga ocupat executant Express.

En el projecte on anem a fer les proves necessitem instal·lar el mòdul axios amb el corresponent comando:

npm install axios

Una vegada instal·lat ho incorporem al fitxer font (o fitxers) on anem a fer els tests (per exemple, tests.js), i realitzem una configuració prèvia on indiquem la URL base a la que volem connectar (aquella on estarà escoltant el projecte a provar), i el tipus de contingut que utilitzarem (en el nostre cas, contingut JSON):

require('dotenv').config();
const axios = require('axios');

const URL_TESTS = process.env.URL_TESTS || 'http://127.0.0.1:8080';

const api = axios.create({
    baseURL: URL_TESTS,
    headers: {
        'Content-Type': 'application/json',
    }, 
    validateStatus: () => true // Captura qualsevol codi d'estat sense llançar excepcions d'Axios
});

A més, declararem una sèrie de variables globals i mètodes generals que ens seran d’ajuda en els diferents tests que realitzem:

let testsTotals = 0;
let testsOK = 0;
let testsError = 0;

function assert(condicio, missatge) {
    if (!condicio) {
        throw new Error(missatge);
    }
}

async function runTest(nom, testFn) {
    testsTotals++;
    try {
        await testFn();
        testsOK++;
        console.log(`[OK]: ${nom}`);
    } catch (err) {
        testsError++;
        console.log(`[ERROR]: ${nom}`);
        console.log(`    Detall: ${err.message}`);
    }
}

1.2. Execució de proves simples

Per a il·lustrar este apartat amb exemples ens basarem en este projecte, que és una versió en GitHub de l’aplicació ContactesREST que hem desenrotllat en documents anteriors. Instal·larem Axios en el projecte i crearem un fitxer tests.js en la carpeta inicial, amb el codi d’inicialització i configuració que hem vist abans.

Afegirem una primera prova per al servici de llistat de contactes (GET /contactes). Podem crear per a això una funció principal on anirem acumulant els tests que vulguem realitzar. Afegim un codi inicial per a verificar la connexió amb el servidor Express abans de res:

async function main() {

    console.log(`Iniciant suite de proves HTTP contra ${URL_TESTS}...`);

    // Verificar si el servidor està en marxa
    try {
        await api.get('/contactes', { timeout: 2500 });
    } catch (err) {
        console.error(`[ERROR]: No es va poder connectar amb el servidor en ${URL_TESTS}.`);
        console.error('Assegura\'t que el servidor Express estiga prèviament alçat i accessible abans d\'executar els tests.\n');
        process.exit(1);
    }

    // Ací anirem col·locant les nostres proves
}

main();

Si la connexió és satisfactòria, afegim eixa primera prova de GET /contactes:

// GET /contactes
await runTest('GET /contactes -> 200 retorna llistat de contactes', async () => {
    const res = await api.get('/contactes');
    assert(res.status === 200, `Esperat 200, rebut ${res.status}`);
    assert(res.data.ok === true, 'Esperat ok: true');
    assert(Array.isArray(res.data.resultat) && res.data.resultat.length > 0, 'Esperat array amb contactes');
});

Observa el funcionament de la prova, perquè es repetirà en unes altres que farem:

Anem ara amb la petició d’inserció:

let idContacteCreat = null;

// POST /contactes (Cas OK)
await runTest('POST /contactes -> 200 crea nou contacte correctament', async () => {
    const cos = {
        nom: 'Contacte Axios',
        edat: 20,
        telefon: '666554400'
    };
    const res = await api.post('/contactes', cos);
    assert(res.status === 200, `Esperat 200, rebut ${res.status}`);
    assert(res.data.ok === true, 'Esperat ok: true');
    assert(res.data.resultat && res.data.resultat._id, 'Esperat objecte contacte creat amb _id');
    assert(res.data.resultat.nome === cos.nom, 'El nom no coincidix');
    idContacteCreat = res.data.resultat._id;
});

En este cas hem definit una variable idContacteCreat per a guardar-nos l’id del contacte, i poder després fer modificacions, cerques o esborraments sobre eixe contacte de prova. Per exemple, així definim les altres tres proves bàsiques sobre el contacte (buscar per ID, modificar i esborrar finalment, per a no deixar rastre del contacte):

// GET /contactes/:id (Cas OK)
await runTest(`GET /contactes/:id -> 200 retorna el contacte pel seu ID`, async () => {
    assert(idContacteCreat !== null, 'Es requerix idContacteCreat');
    const res = await api.get(`/contactes/${idContacteCreat}`);
    assert(res.status === 200, `Esperat 200, rebut ${res.status}`);
    assert(res.data.ok === true, 'Esperat ok: true');
    assert(res.data.resultat._id === idContacteCreat, 'ID retornat no coincidix');
});

// PUT /contactes/:id (Cas OK)
await runTest(`PUT /contactes/:id -> 200 actualitza dades del contacte`, async () => {
    assert(idContacteCreat !== null, 'Es requerix idContacteCreat');
    const actualitzacio = { nom: 'Contacte Axios Modificat', edat: 21, telefon: '666554400' };
    const res = await api.put(`/contactes/${idContacteCreat}`, actualitzacio);
    assert(res.status === 200, `Esperat 200, rebut ${res.status}`);
    assert(res.data.ok === true, 'Esperat ok: true');
    assert(res.data.resultat.nom === actualizacion.nom, 'Nom actualitzat no coincidix');
});

// DELETE /contactes/:id (Cas OK)
await runTest(`DELETE /contactes/:id -> 200 elimina el contacte de prova`, async () => {
    assert(idContacteCreat !== null, 'Es requerix idContacteCreat');
    const res = await api.delete(`/contactes/${idContacteCreat}`);
    assert(res.status === 200, `Esperat 200, rebut ${res.status}`);
    assert(res.data.ok === true, 'Esperat ok: true');
    assert(res.data.resultat._id === idContacteCreat, 'ID eliminat no coincidix');
});

A més, podem definir proves errònies i verificar que, efectivament falla el servici. Per exemple, així intentem inserir un contacte equivocat, o buscar un amb un id inventat que no existix:

// POST /contactes (Cas Error: dades invàlides)
await runTest('POST /contactes -> 400 error per dades no vàlides (edat massa gran)', async () => {
    const cos = {
        nom: 'Contacte Axios Erroni',
        edat: 150,
        telefon: '666005544'
    };
    const res = await api.post('/contactes', cos);
    assert(res.status === 400, `Esperat 400, rebut ${res.status}`);
    assert(res.data.ok === false, 'Esperat ok: false');
});

// GET /contactes/:id (Cas Error: ID inexistent)

const fakeId = '507f1f77bcf86cd799439011';

await runTest(`GET /contactes/${fakeId} -> 400 contacte inexistent`, async () => {
    const res = await api.get(`/contactes/${fakeId}`);
    assert(res.status === 400, `Esperat 400, rebut ${res.status}`);
    assert(res.data.ok === false, 'Esperat ok: false');
});

1.3. Execució de proves amb autenticació

Si necessitem executar proves amb autenticació per tokens, necessitem afegir alguns tests més al nostre conjunt.

D’una banda, necessitem comprovar un login correcte i incorrecte i, en el primer cas, guardar-nos el token en una variable dins del mètode main per a poder-lo usar en altres proves que requerisquen autenticació:

let token = null;

// POST /auth/login (Cas Error: credencials incorrectes)
await runTest('POST /auth/login -> 401 error amb credencials incorrectes', async () => {
    const res = await api.post('/auth/login', {
        login: 'usuari_inexistent',
        password: 'password_incorrecte'
    });
    assert(res.status === 401, `Esperat 401, rebut ${res.status}`);
    assert(res.data.ok === false, 'Esperat ok: false');
    assert(!res.data.token, 'No hauria de retornar token en login fallit');
});

// POST /auth/login (Cas OK: credencials vàlides)
await runTest('POST /auth/login -> 200 retorna token vàlid', async () => {
    const res = await api.post('/auth/login', {
        login: TEST_USER,
        password: TEST_PASS
    });
    assert(res.status === 200, `Esperat 200, rebut ${res.status}`);
    assert(res.data.ok === true, 'Esperat ok: true');
    assert(typeof res.data.token === 'string' && res.data.token.length > 0, 'S\'esperava un token en la resposta');

    token = res.data.token;
});

Ara que ja tenim el token de la prova exitosa, ho passem en la capçalera Authorization de les proves que ho requerisquen. Per exemple, així definiríem dos proves POST: una incorrecta per no enviar token d’autorització, i una altra correcta:

// POST /contactes (Cas Error: no s'envia token d'autorització)
await runTest('POST /contactes -> 401 rebutjat si no s\'envia token', async () => {
    const cos = { nom: 'Sense Token', edat: 25, telefon: '666112233' };
    const res = await api.post('/contactes', cos);
    assert(res.status === 401, `Esperat 401, rebut ${res.status}`);
    assert(res.data.ok === false, 'Esperat ok: false');
});

// POST /contactes (Cas OK amb token)
await runTest('POST /contactes -> 200 crea nou contacte correctament (amb token)', async () => {
    assert(token !== null, 'Es requerix un token vàlid del test de login');
    const cos = {
        nom: 'Contacte Axios',
        edat: 20,
        telefon: '666554400'
    };
    const res = await api.post('/contactes', cos, {headers: { Authorization: `Bearer ${token}`);
    assert(res.status === 200, `Esperat 200, rebut ${res.status}`);
    assert(res.data.ok === true, 'Esperat ok: true');
    assert(res.data.resultat && res.data.resultat._id, 'Esperat objecte contacte creat amb _id');
    assert(res.data.resultat.nom === cos.nom, 'El nom no coincidix');
    idContacteCreat = res.data.resultat._id;
});

2. Testing integrat en repositoris i CI/CD

Per completar