Integração e testes de ponta a ponta facilitados com Node.js e MongoDB

Publicados: 2022-03-11

Os testes são uma parte essencial da construção de um aplicativo Node.js robusto. Testes adequados podem facilmente superar muitas deficiências que os desenvolvedores podem apontar sobre as soluções de desenvolvimento Node.js.

Embora muitos desenvolvedores se concentrem em 100% de cobertura com testes de unidade, é importante que o código que você escreve não seja apenas testado isoladamente. Os testes de integração e de ponta a ponta oferecem essa confiança extra ao testar partes do seu aplicativo juntas. Essas partes podem funcionar bem sozinhas, mas em um sistema grande, as unidades de código raramente funcionam separadamente.

Node.js e MongoDB juntos formam uma das duplas mais populares dos últimos tempos. Se acontecer de você ser uma das muitas pessoas que os usam, você está com sorte.

Neste artigo, você aprenderá a escrever testes de integração e de ponta a ponta facilmente para seu aplicativo Node.js e MongoDB que são executados em instâncias reais do banco de dados, tudo sem a necessidade de configurar um ambiente elaborado ou código complicado de configuração/desmontagem .

Você verá como o pacote mongo-unit ajuda na integração e nos testes de ponta a ponta no Node.js. Para obter uma visão geral mais abrangente dos testes de integração do Node.js, consulte este artigo.

Lidando com um banco de dados real

Normalmente, para testes de integração ou de ponta a ponta, seus scripts precisarão se conectar a um banco de dados dedicado real para fins de teste. Isso envolve escrever código que é executado no início e no final de cada caso/conjunto de teste para garantir que o banco de dados esteja em um estado previsível limpo.

Isso pode funcionar bem para alguns projetos, mas tem algumas limitações:

  • O ambiente de teste pode ser bastante complexo. Você precisará manter o banco de dados rodando em algum lugar. Isso geralmente requer um esforço extra para configurar com servidores CI.
  • O banco de dados e as operações podem ser relativamente lentos. Como o banco de dados usará conexões de rede e as operações exigirão atividade do sistema de arquivos, pode não ser fácil executar milhares de testes rapidamente.
  • O banco de dados mantém o estado e não é muito conveniente para testes. Os testes devem ser independentes uns dos outros, mas usar um banco de dados comum pode fazer com que um teste afete outros.

Por outro lado, usar um banco de dados real torna o ambiente de teste o mais próximo possível da produção. Isso pode ser visto como uma vantagem particular dessa abordagem.

Usando um banco de dados real na memória

Usar um banco de dados real para testes parece ter alguns desafios. Mas, a vantagem de usar um banco de dados real é boa demais para ser repassada. Como podemos contornar os desafios e manter a vantagem?

Reutilizar uma boa solução de outra plataforma e aplicá-la ao mundo Node.js pode ser o caminho a seguir.

Os projetos Java usam amplamente o DBUnit com um banco de dados na memória (por exemplo, H2) para essa finalidade.

O DBUnit é integrado ao JUnit (o executor de testes Java) e permite definir o estado do banco de dados para cada conjunto de testes/teste, etc. Ele remove as restrições discutidas acima:

  • DBUnit e H2 são bibliotecas Java, portanto, você não precisa configurar um ambiente extra. Tudo roda na JVM.
  • O banco de dados na memória torna esse gerenciamento de estado muito rápido.
  • O DBUnit torna a configuração do banco de dados muito simples e permite manter um estado claro do banco de dados para cada caso.
  • H2 é um banco de dados SQL e é parcialmente compatível com MySQL, então, na maioria dos casos, o aplicativo pode trabalhar com ele como com um banco de dados de produção.

Partindo desses conceitos, decidi fazer algo semelhante para Node.js e MongoDB: Mongo-unit.

Mongo-unit é um pacote Node.js que pode ser instalado usando NPM ou Yarn. Ele executa o MongoDB na memória. Ele facilita os testes de integração integrando-se bem com o Mocha e fornecendo uma API simples para gerenciar o estado do banco de dados.

A biblioteca usa o pacote NPM pré-compilado do mongodb, que contém binários do MongoDB pré-compilados para os sistemas operacionais populares. Essas instâncias do MongoDB podem ser executadas no modo de memória.

Instalando a unidade Mongo

Para adicionar mongo-unit ao seu projeto, você pode executar:

 npm install -D mongo-unit

ou

 yarn add mongo-unit

E, é isso. Você nem precisa do MongoDB instalado em seu computador para usar este pacote.

Usando o Mongo-unit para testes de integração

Vamos imaginar que você tenha um aplicativo Node.js simples para gerenciar tarefas:

 // service.js const mongoose = require('mongoose') const mongoUrl = process.env.MONGO_URL || 'mongodb://localhost:27017/example' mongoose.connect(mongoUrl) const TaskSchema = new mongoose.Schema({ name: String, started: Date, completed: Boolean, }) const Task = mongoose.model('tasks', TaskSchema) module.exports = { getTasks: () => Task.find(), addTask: data => new Task(data).save(), deleteTask: taskId => Task.findByIdAndRemove(taskId) }

A URL de conexão do MongoDB não é codificada aqui. Como acontece com a maioria dos back-ends de aplicativos da Web, estamos tirando isso da variável de ambiente. Isso nos permitirá substituí-lo por qualquer URL durante os testes.

 const express = require('express') const bodyParser = require('body-parser') const service = require('./service') const app = express() app.use(bodyParser.json()) app.use(express.static(`${__dirname}/static`)) app.get('/example', (req, res) => { service.getTasks().then(tasks => res.json(tasks)) }) app.post('/example', (req, res) => { service.addTask(req.body).then(data => res.json(data)) }) app.delete('/example/:taskId', (req, res) => { service.deleteTask(req.params.taskId).then(data => res.json(data)) }) app.listen(3000, () => console.log('started on port 3000'))

Este é um trecho de um aplicativo de exemplo que possui uma interface de usuário. O código para a interface do usuário foi omitido por questões de brevidade. Você pode conferir o exemplo completo no GitHub.

Integrando com Mocha

Para fazer o Mocha executar testes de integração no mongo-unit, precisamos executar a instância do banco de dados mongo-unit antes que o código do aplicativo seja carregado no contexto Node.js. Para fazer isso, podemos usar o parâmetro mocha --require e a biblioteca Mocha-prepare, que permite realizar operações assíncronas nos scripts require.

 // it-helper.js const prepare = require('mocha-prepare') const mongoUnit = require('mongo-unit') prepare(done => mongoUnit.start() .then(testMongoUrl => { process.env.MONGO_URL = testMongoUrl done() }))

Escrevendo testes de integração

A primeira etapa é adicionar um teste ao banco de dados de teste ( testData.json ):

 { "tasks": [ { "name": "test", "started": "2017-08-28T16:07:38.268Z", "completed": false } ] }

O próximo passo é adicionar os próprios testes:

 const expect = require('chai').expect const mongoose = require('mongoose') const mongoUnit = require('../index') const service = require('./app/service') const testMongoUrl = process.env.MONGO_URL describe('service', () => { const testData = require('./fixtures/testData.json') beforeEach(() => mongoUnit.initDb(testMongoUrl, testData)) afterEach(() => mongoUnit.drop()) it('should find all tasks', () => { return service.getTasks() .then(tasks => { expect(tasks.length).to.equal(1) expect(tasks[0].name).to.equal('test') }) }) it('should create new task', () => { return service.addTask({ name: 'next', completed: false }) .then(task => { expect(task.name).to.equal('next') expect(task.completed).to.equal(false) }) .then(() => service.getTasks()) .then(tasks => { expect(tasks.length).to.equal(2) expect(tasks[1].name).to.equal('next') }) }) it('should remove task', () => { return service.getTasks() .then(tasks => tasks[0]._id) .then(taskId => service.deleteTask(taskId)) .then(() => service.getTasks()) .then(tasks => { expect(tasks.length).to.equal(0) }) }) })

E, voilá!

Observe como há apenas algumas linhas de código lidando com configuração e desmontagem.

Como você pode ver, é muito fácil escrever testes de integração usando a biblioteca mongo-unit. Nós não zombamos do próprio MongoDB e podemos usar os mesmos modelos do Mongoose. Temos controle total dos dados do banco de dados e não perdemos muito no desempenho dos testes, pois o MongoDB falso está sendo executado na memória.

Isso também nos permite aplicar as melhores práticas de teste de unidade para testes de integração:

  • Faça cada teste independente de outros testes. Carregamos dados novos antes de cada teste, dando-nos um estado totalmente independente para cada teste.
  • Use o estado mínimo necessário para cada teste. Não precisamos preencher todo o banco de dados. Só precisamos definir os dados mínimos necessários para cada teste específico.
  • Podemos reutilizar uma conexão para o banco de dados. Aumenta o desempenho do teste.

Como bônus, podemos até executar o próprio aplicativo contra o mongo-unit. Ele nos permite fazer testes de ponta a ponta para nosso aplicativo em um banco de dados simulado.

Testes de ponta a ponta com Selenium

Para testes de ponta a ponta, usaremos o Selenium WebDriver e o executor de testes Hermione E2E.

Primeiro, vamos inicializar o driver e o executor de testes:

 const mongoUnit = require('mongo-unit') const selenium = require('selenium-standalone') const Hermione = require('hermione') const hermione = new Hermione('./e2e/hermione.conf.js') //hermione config seleniumInstall() //make sure selenium is installed .then(seleniumStart) //start selenium web driver .then(mongoUnit.start) // start mongo unit .then(testMongoUrl => { process.env.MONGO_URL = testMongoUrl //store mongo url }) .then(() => { require('./index.js') //start application }) .then(delay(1000)) // wait a second till application is started .then(() => hermione.run('', hermioneOpts)) // run hermiona e2e tests .then(() => process.exit(0)) .catch(() => process.exit(1))

Também precisaremos de algumas funções auxiliares (o tratamento de erros foi removido por questões de brevidade):

 function seleniumInstall() { return new Promise(resolve => selenium.install({}, resolve)) } function seleniumStart() { return new Promise(resolve => selenium.start(resolve)) } function delay(timeout) { return new Promise(resolve => setTimeout(resolve, timeout)) }

Após preencher o banco de dados com alguns dados e limpá-lo assim que os testes estiverem concluídos, podemos executar nossos primeiros testes:

 const expect = require('chai').expect const co = require('co') const mongoUnit = require('../index') const testMongoUrl = process.env.MONGO_URL const DATA = require('./fixtures/testData.json') const ui = { task: '.task', remove: '.task .remove', name: '#name', date: '#date', addTask: '#addTask' } describe('Tasks', () => { beforeEach(function () { return mongoUnit.initDb(testMongoUrl, DATA) .then(() => this.browser.url('http://localhost:3000')) }) afterEach(() => mongoUnit.dropDb(testMongoUrl)) it('should display list of tasks', function () { const browser = this.browser return co(function* () { const tasks = yield browser.elements(ui.task) expect(tasks.length, 1) }) }) it('should create task', function () { const browser = this.browser return co(function* () { yield browser.element(ui.name).setValue('test') yield browser.element(ui.addTask).click() const tasks = yield browser.elements(ui.task) expect(tasks.length, 2) }) }) it('should remove task', function () { const browser = this.browser return co(function* () { yield browser.element(ui.remove).click() const tasks = yield browser.elements(ui.task) expect(tasks.length, 0) }) }) })

Como você pode ver, os testes de ponta a ponta são muito semelhantes aos testes de integração.

Embrulhar

A integração e os testes de ponta a ponta são importantes para qualquer aplicativo de grande escala. Os aplicativos Node.js, em particular, podem se beneficiar enormemente dos testes automatizados. Com o mongo-unit, você pode escrever integração e testes de ponta a ponta sem se preocupar com todos os desafios que acompanham esses testes.

Você pode encontrar exemplos completos de como usar o mongo-unit no GitHub.

Leitura adicional no Blog da Toptal Engineering:

  • Criando uma API REST Node.js/TypeScript, Parte 3: MongoDB, autenticação e testes automatizados