Seus primeiros tests unitários com Javascript

Carlos Costa

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&paras=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

Referencias