Algo que eu sempre ouço é: “testes automatizados são muito difíceis!”. Sim, eu concordo. Escrever testes não é uma tarefa trivial. Mas não é tão difícil! Testes podem ficar significamente mais complicados se sua aplicação não é testável. As pessoas me perguntam: “Como posso testar minha aplicação?”. Mas o que elas realmente deveria estar perguntando é: “como posso deixar minha aplicação testável?”.

Mas o que eu quero dizer com “testável”? Testes consistem de três simples partes: Arranjo, ação e asserção. O “arranjo” e “asserção” podem ser bem complicadas se o seu código é muito acoplado. Vamos ver um exemplo:

const express = require('express')
const multer = require('multer')
const AWS = require('aws-sdk')

require('dotenv').config()

AWS.config.update({ region: process.env.AWS_REGION })
const s3 = new AWS.S3({ apiVersion: '2006-03-01' })

const app = express()
const upload = multer()
const port = 3000

app.post('/upload', upload.single('file'), (req, res) => {
  const file = req.file

  s3.upload(
    {
      Bucket: process.env.AWS_BUCKET,
      Key: file.originalname,
      Body: file.buffer
    },
    (err, data) => {
      if (err) {
        res.status(500).send(err)
        return
      }

      res.send('')
    }
  )
})

if (process.env.NODE_ENV !== 'test') {
  app.listen(port, () => {
    console.log(`Listening on port ${port}`)
  })
}

module.exports = app

Este é um código relativamente simples. Ele recebe um arquivo enviado e reenvia para o S3 da AWS. Agora, vamos ver como podemos testar isso:

const app = require('../index')
const request = require('supertest')
const AWS = require('aws-sdk')

jest.mock('aws-sdk')

it('deve enviar arquivo para s3', async () => {
  const S3Client = AWS.S3.mock.instances[0]
  S3Client.upload.mockImplementation((params, callback) => callback(null, null))

  await request(app)
    .post('/upload')
    .attach('file', 'test/fixtures/image.jpeg')
    .expect(200)

  expect(S3Client.upload).toHaveBeenCalledWith(
    expect.objectContaining({
      Key: 'image.jpeg'
    }),
    expect.any(Function)
  )
})

O código sabe demais

Qual o problema com o código acima? Ele simplesmente sabe demais. Ele sabe como receber uma requisição http e como respondê-la e também os detalhes de como fazer o upload para a S3. Os detalhes de como o SDK da AWS funciona estão vazando para nosso código, e nós não queremos isso. Em um cenário ideal, cada função deve saber como uma e apenas uma coisa funciona.

Coupled

Como podemos nos livrar deste problema? A resposta é: delineando linhas claras entre nosso código e o SDK da AWS.

Uncoupled

Então, vamos refatorar nosso método de upload em uma forma mais desacoplada:

index.js:

const express = require('express')
const multer = require('multer')
const { uploadFile } = require('./s3_client')

require('dotenv').config()

const app = express()
const upload = multer()
const port = 3000

app.post('/upload', upload.single('file'), async (req, res) => {
  const file = req.file

  try {
    await uploadFile(file.originalname, file.buffer)
    res.send('')
  } catch (e) {
    res.status(500).send(e)
    return
  }
})

if (process.env.NODE_ENV !== 'test') {
  app.listen(port, () => {
    console.log(`Listening on port ${port}`)
  })
}

module.exports = app

 

s3_client.js:

const AWS = require('aws-sdk')

AWS.config.update({ region: process.env.AWS_REGION })
const s3 = new AWS.S3({ apiVersion: '2006-03-01' })

module.exports = {
  async uploadFile (name, data) {
    await new Promise((resolve, reject) => {
      s3.upload(
        {
          Bucket: process.env.AWS_BUCKET,
          Key: name,
          Body: data
        },
        (err, data) => {
          if (err) {
            reject(err)
            return
          }

          resolve()
        }
      )
    })
  }
}

Nós movemos todoa a logica de “como enviar um arquivo para a S3” para a função uploadFile. O método /upload não se importa como o arquivo é enviado para a S3. Ele apenas se importa se o arquivo foi enviado ou não. Consequentemente, podemos ver que testar esta versão do nosso método é muito mais simples:

const app = require('../index')
const request = require('supertest')
const { uploadFile } = require('../s3_client')

jest.mock('../s3_client')

it('deve enviar arquivo para s3', async () => {
  await request(app)
    .post('/upload')
    .attach('file', 'test/fixtures/image.jpeg')
    .expect(200)

  expect(uploadFile).toHaveBeenCalledWith('image.jpeg', expect.any(Buffer))
})

 

Como podemos ver, nós deixamos o nosso teste mais simples e o nosso código mais limpo no processo. Ganhamos em ambos os lados. E isso é o que normalmente acontece.

Testar o seu código não vai o fazer mais limpo por si só, porém deixando o seu código mais fácil de testar provavelmente vai. Fique atento a testes complexos: eles geralmente são um sinal de que seu código é muito acoplado.