Seus primeiros tests unitários com Javascript
O objetivo desse post é iniciar de forma simples e rápida com práticas de testes unitários no Javascript. Vamos definir o nosso problema, configurar o nosso projeto e começar a testar.
Problema
Nosso problema será criar um módulo para fazer o cacheamento de requisições http, a requisição será feita uma vez, armazenada em cache interno e retornada diretamente do cache sempre que for chamada novamente.
Configurando o projeto
Agora vamos configurar um projeto javascript básico. Para fazer requisições http vamos usar a lib node-fetch e para testar nosso código vamos usar o AVA test runner.
Primeiro vamos criar a pasta do nosso projeto.
mkdir cacheable
Iniciando o npm.
npm init -y
Instalando dependências.
npm install node-fetch
Instalando e configurando o AVA.
npm init ava
Criando arquivos para nossa classe e nosso teste.
touch cacheable.js cacheable.test.js
Feito isso, é assim que nosso projeto vai ficar.
├── 📂 cacheable/
│ ├── 📄 cacheable.js
│ ├── 📄 cacheable.test.js
│ ├── 📄 package-lock.json
│ ├── 📄 package.json
Precisamos também configurar o nosso package.json
adicionando o atributo "type: module"
,
essa configuração é necessária para importar nossas dependências via ES Modules.
{
"type": "module",
"scripts": {
"test": "ava"
},
"dependencies": {
"node-fetch": "^3.2.10"
},
"devDependencies": {
"ava": "^5.0.1"
}
}
Agora vamos escrever um teste básico para verificar se está tudo certo com o nosso projeto.
//cachable.test.js
import test from 'ava'
test('check initial config', (t) => {
t.is(true, true)
})
Executando com o comando npm test
:
❯ npm test
✔ check initial config
Tudo ok, agora vamos para nossos testes/solução.
Iniciando com os testes
Primeiro vamos definir a estutura da nossa classe CacheableFetch
.
//cachable.js
export class CacheableFetch {}
Vamos precisar do atributo .cache
e do métodos .get()
e invalidate()
.
.cache
— collection que vai armazenar nossas requisições;.get()
— método que será usado para fazer requisições;.invalidate()
— método que será usado para invalidar nosso cache.
Primeiro vamos escrever nossos testes e depois a nossa implementação.
//cachable.test.js
import test from 'ava'
import { CacheableFetch } from './cacheable.js'
test('CacheableFetch: should have .cache, .get and .invalidate', (t) => {
const HTTP = new CacheableFetch()
t.truthy(HTTP.cache)
t.truthy(HTTP.get)
t.truthy(HTTP.invalidate)
})
Executando👇
❯ npm test
✘ [fail]: Should have .cache atribute and .get and .invalidate methods
Obviamente que o teste vai falhar, mas essa é a intenção. Nossos testes serão sempre escritos primeiro e logo em seguida implementamos a solução.
Nosso processo será o seguinte:
- 🔴 escrever os testes;
- 🟡 escrever a solução que vai passar pelos testes;
- 🟢 refatorar nosso código caso necessário.
Continuando com o nosso código, vamos implementar a estrutura da nossa classe.
//cachable.js
class CacheableFetch {
cache = new Map()
get() {}
invalidate() {}
}
Executando 👇
❯ npm test
✔ Should have .cache atribute and .get and .invalidate methods
Tudo ok. Agora vamos implementar os testes para o nosso método .get()
.
O método .get()
vai fazer uma requisição http e retornar o seguinte objeto:
{
status: 'first time' | 'cached',
data: response.data
}
Nesse primeiro teste vamos verificar se o status
e o HTTP.cache.size
estão
com os valores esperados.
//cachable.test.js
test('Should return correct status and cache size', async (t) => {
const HTTP = new CacheableFetch()
const res1 = await HTTP.get(endpoint1)
const res2 = await HTTP.get(endpoint1)
const res3 = await HTTP.get(endpoint2)
t.is(res1.status, 'first time')
t.is(res2.status, 'cached')
t.is(res3.status, 'first time')
t.is(HTTP.cache.size, 2)
})
Implementando a solução: 👇
//cachable.js
import fetch from 'node-fetch'
export class CacheableFetch {
cache = new Map()
async get(endpoint) {
const result = await fetch(endpoint)
const data = await result.json()
this.cache.set(endpoint, data)
return {
data: data,
status: 'first time',
}
}
invalidate() {}
}
Rodando os testes: 👇
❯ npm test
✔ Should have .cache atribute and .get and .invalidate methods
✔ Should return correct status and cache size (2.5s)
Agora que o processo de escrita dos testes/solução ficou mais claro, vamos visualizar de forma completa a nossa classe e os nossos testes.
Solução final
Classe CacheableFetch
.
//cachable.js
import fetch from 'node-fetch'
export class CacheableFetch {
cache = new Map()
async get(endpoint) {
if (this.cache.has(endpoint)) {
return {
data: this.cache.get(endpoint),
status: 'cached',
}
}
const result = await fetch(endpoint)
const data = await result.json()
this.cache.set(endpoint, data)
return {
data: data,
status: 'first time',
}
}
invalidate(path) {
this.cache.delete(path)
}
}
Testes.
import test from 'ava'
import { CacheableFetch } from './cacheable.js'
let endpoint1 = 'https://baconipsum.com/api/?type=meat-and-filler'
let endpoint2 =
'https://baconipsum.com/api/?type=all-meat¶s=2&start-with-lorem=1'
let endpoint3 =
'https://baconipsum.com/api/?type=all-meat&sentences=1&start-with-lorem=1'
test('CacheableFetch: should have .cache, .get and .invalidate', (t) => {
const HTTP = new CacheableFetch()
t.truthy(HTTP.cache)
t.truthy(HTTP.get)
t.truthy(HTTP.invalidate)
})
test('Should return correct status and cache size', async (t) => {
const HTTP = new CacheableFetch()
const res1 = await HTTP.get(endpoint1)
const res2 = await HTTP.get(endpoint1)
const res3 = await HTTP.get(endpoint2)
t.is(res1.status, 'first time')
t.is(res2.status, 'cached')
t.is(res3.status, 'first time')
t.is(HTTP.cache.size, 2)
})
test('Cached response data should be the same of uncached responde data', async (t) => {
const HTTP = new CacheableFetch()
const res = await HTTP.get(endpoint1)
const resCached = await HTTP.get(endpoint1)
t.is(res.status, 'first time')
t.is(resCached.status, 'cached')
t.deepEqual(resCached.data.join(), res.data.join())
})
test('Should store multiples requests', async (t) => {
const HTTP = new CacheableFetch()
await HTTP.get(endpoint1)
await HTTP.get(endpoint2)
await HTTP.get(endpoint3)
t.is(HTTP.cache.size, 3)
})
test('Should get request by cache', async (t) => {
const HTTP = new CacheableFetch()
await HTTP.get(endpoint1)
await HTTP.get(endpoint2)
await HTTP.get(endpoint3)
const res1 = await HTTP.get(endpoint1)
const res2 = await HTTP.get(endpoint2)
const res3 = await HTTP.get(endpoint3)
t.is(res1.status, 'cached')
t.is(res2.status, 'cached')
t.is(res3.status, 'cached')
})
test('Should invalidate a request cached', async (t) => {
const HTTP = new CacheableFetch()
const res1 = await HTTP.get(endpoint1)
t.is(res1.status, 'first time')
const res2 = await HTTP.get(endpoint1)
t.is(res2.status, 'cached')
HTTP.invalidate(endpoint1)
const res3 = await HTTP.get(endpoint1)
t.is(res3.status, 'first time')
})
Rodando todos os testes: 👇
- verificar a estutura da classe
CacheableFetch
; - verificar se os dados cacheados e não cacheados são os mesmos;
- verificar o status das requisições e o cache size;
- verificar se os dados cacheados estão sendo realmente retornados;
- verificar o cacheamento de várias requisições;
- verificar a invalidação de uma requisição.
❯ npm test
✔ CacheableFetch: should have .cache, .get and .invalidate
✔ Cached response data should be the same of uncached responde data (2.4s)
✔ Should return correct status and cache size (4.6s)
✔ Should invalidate a request cached (6.5s)
✔ Should store multiples requests (8s)
✔ Should get request by cache (8s)
🐙 Código completo no github: https://github.com/carllosnc/lab/tree/master/cacheable-js