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:
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.
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}`);
}
}
testsTotals), i quants hi ha hagut satisfactoris (testsOK) i erronis (testsError).assert definix el comportament de les assercions. Una asserció és una comprovació que es fa sobre el resultat d’una operació, i que pot ser verdadera o falsa. Per exemple, comprovar després d’una inserció que el codi d’estat que es retorna és 201. En cas que no siga així, es pot llançar una excepció amb un missatge determinat.runTest permetrà llançar diferents funcions (passades com a paràmetre) que seran els tests que volem realitzar. Si tot va bé es comptarà com a test exitós, i si alguna cosa falla i es produïx una excepció es comptarà com a test equivocatPer 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:
api, amb el qual hem inicialitzat Axiosres), comprovem les assercions que vulguem. En este cas, que el codi d’estat siga correcte, que es retorne el booleà a true i que obtinguem un array amb resultats. Si alguna cosa d’això falla, el mètode assert provocarà una excepció, amb el que el mètode runTest comptabilitzarà el test com a erroni, mostrant el missatge corresponent.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');
});
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;
});
Per completar