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.
Como podemos nos livrar deste problema? A resposta é: delineando linhas claras entre nosso código e o SDK da AWS.
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.