Skip to content

Instantly share code, notes, and snippets.

@nicolas-oliveira
Last active April 19, 2020 01:12
Show Gist options
  • Save nicolas-oliveira/82bfe90e64cc2ee9a95c61949e060525 to your computer and use it in GitHub Desktop.
Save nicolas-oliveira/82bfe90e64cc2ee9a95c61949e060525 to your computer and use it in GitHub Desktop.

Funcionalidades avançadas

  • Adicionando a validação
  • Adicionando Testes
    • Por que fazer testes?
    • TDD
    • Configurando Jest
    • Tipos de testes
    • Configurando banco de testes
    • Instalando supertest
    • Testando rota de autenticação
  • Deploy
    • Alternativas
    • Qual escolher?
  • Estudos daqui pra frente
    • Padrões de código: ESLint, Prettier
    • Autenticação JWT
    • Styled Components
  • Dicas para aproveitar melhor
    • Github
    • Linkedin

1) Adicionando a validação

A validação de dados no backend é importante pois é o local onde será armazenado todos os dados da aplicação. Nenhuma informação vinda de formulários.

Um dos pacotes mais famosos em validação de dados é o JOI.

O JOI é um validador de esquemas que é um processo que dá a diferença entre o esquema de banco de dados existente e o esquema necessário para fazer a aplicação atual funcionar.

hapi.dev

O primeiro passo para esta parte da aplicação será instalando o pacote celebrate, que é a integração do JOI com o Express

$ npm install celebrate

arb/celebrate

routes.js

routes.post('/ongs', celebrate({}) ,OngController.create);

É necessário fazer a validação antes da criação do esquema. O express usa o conceito de middleware, fazendo a validação na ordem dos parâmetros passados dentro da requisição.

routes.js

routes.post('/ongs', celebrate({
		[Segments.BODY]: Joi.object().keys({
        (..Corpo da requisição..)
    })
}) ,OngController.create);

Caso seja necessário verificar o header a estrutura está no .object({}).unknown(), sendo o unknown a função responsável por descartar os outros parâmetros da requisição http tornando ela restritiva porém não impedindo todas as requisições.

routes.get('/profile', celebrate({
    [Segments.HEADERS]: Joi.object({
        authorization: Joi.string().required(),
    }).unknown()
}),ProfileController.index);

Exemplo do corpo da requisição no Insomnia
{
	"name": "Cachorros e CIA",
	"email": "nicolasm15@hotmail.com.br",
	"whatsapp": "3511111111",
	"city": "Rio de Janeiro",
	"uf": "RJ"
}

routes.js

routes.post('/ongs', celebrate({
    [Segments.BODY]: Joi.object().keys({
        name: Joi.string().required(),
        email: Joi.string().required().email(),
        whatsapp: Joi.string().required().min(10).max(11),
        city: Joi.string().required(),
        uf: Joi.string().required().length(2),
    })
}) ,OngController.create);

Agora no insomnia colocando algum dos dados que descumprem com as regras impostas:

[500 Internal Server Error]
Error: "uf" length must be 2 characters long
    at /home/nicolas/algoritmos/javascript/git.projects/oministack11.master/backend/node_modules/celebrate/lib/index.js:103:13
    at processTicksAndRejections (internal/process/task_queues.js:97:5)

Para evitar um erro genérico como o acima deveremos criar dentro arquivo index.js

const { errors } = require('celebrate');

(...)
(....Rotas)
app.use(errors());

Desta forma ele irá criar uma estrutura mais robusta como esta abaixo:

{
 "statusCode": 400,
 "error": "Bad Request",
 "message": "\"uf\" length must be 2 characters long",
 "validation": {
  "source": "body",
  "keys": [
   "uf"
  ]
 }
}

2) Adicionando Testes

a) Por que fazer testes?

É necessário fazer testes em todas as páginas da aplicação. Garantindo assim que após alguma alteração de alguma parte do código todo o restante estará intacto.

b) TDD

Teste-Driven Development, que traduzindo para o português Desenvolvimento Dirigido à Testes. Normalmente os testes consistem em basicamente escrever todo o código e após isso criar os testes para garantir que o mesmo estará funcionando adequadamente. Outra forma é criar o teste antes do código para direcionar o código sem erros, já que os relacionamentos estarão bem definidos.

  • Primeiro faz-se as validações e testes para certificar que as regras de negócios estão corretas

c) Configurando Jest

Jest é um framework de testes automatizados

Globals · Jest

$ npm install jest -D

$ npx jest --init

Ele irá pedir algumas opções no qual por enquanto usaremos estas:

The following questions will help Jest to create a suitable configuration for your project

✔ Would you like to use Jest when running "test" script in "package.json"? … yes
✔ Choose the test environment that will be used for testing › node
✔ Do you want Jest to add coverage reports? … no
✔ Automatically clear mock calls and instances between every test? … yes

Irá criar um arquivo jest.config.js na raiz do projeto.

Para executar os testes o comando é basicamente o que iremos configurar dentro de package.json:

$ npm test

d) Tipos de testes

Basicamente existem dois tipos de testes no jest: integration e unit Crie uma pasta chamada tests dessa forma:

 raiz
+   └── tests
+       ├── integration
+       └── unit

A integration será responsável por testar todas as rotas de forma completa da aplicação. A unit será responsável por fazer os teste de forma isolada.

Basicamente a sintaxe do jest consiste em executar um código no qual esperamos algum resultado. A função describe() irá descrever o que o script em si fará, it() é o que deverá ser esperado em expect() que por sua vez possui o código da manipulação do teste.

describe('Sum one number to another', () => {
    it('should get result the sum of two numbers',() => {
        expect(2 + 2).toBe(4);
    });
});

O primeiro teste iremos usar a rota de geração de ID único das ONG's, o nome do arquivo recebe o .spec anteriormente ao .js

raiz
   └── tests
       ├── integration
       └── unit
+          └── generateUniqueID.spec.js

Para facilitar irei isolar em um módulo para importar no script do backend:

/project
└── /src
		├── /contollers
    ├── index.js
    ├── routes.js
    ├── /database
    ├── tests
		│   └── generateUniqueID.spec.js
    └── utils
        └── generateUniqueID.js

const crypto = require('crypto');

module.exports =  function generateUniqueID() {
    return crypto.randomBytes(4).toString('HEX');
}

Para fazer o teste automatizado deveremos verificar qual o padrão do código e o que esperamos que ele gere. No caso do generateUniqueID() o código gerado pelo crypto por exemplo: e1e7ba99 tem 8 caracteres, por isso, podemos exigir que o generateUniqueID() deve ter 8 caracteres. Isso será feito através da função .toHaveLength().

const generateUniqueID = require('../../src/utils/generateUniqueID');

describe('Generate Unique ID',() => {
    it('should generate an unique ID',() => {
        const id = generateUniqueID();

        expect(id).toHaveLength(8);
    });
});

e) Configurando banco de testes

É necessário um banco de dados específico para os testes para que não ocorra nenhuma alteração no banco de dados da aplicação em produção. Dessa forma, dentro da conexão do banco de dados do knex, podemos conectá-lo a um banco de dados de testes.

Dentro do knexfile.js:

test: {
    client: 'sqlite3',
    connection: {
      filename: './src/database/tests.sqlite'
    },
    useNullAsDefault: true,
    migrations: {
      directory: './src/database/migrations'
    },
  },

Instale o pacote que será responsável por mudar o ambiente para testes:

$ npm install cross-env

Dentro do package.json altere o script test:

"scripts": {
    "start": "nodemon src/index.js",
    "test": "cross-env NODE_ENV=test jest"
  },

Dentro do arquivo connection.js podemos definir quando a variável de ambiente será teste ou de desenvolvimento:

const config = process.env.NODE_ENV === 'test' ? configuration.test : configuration.development;

const connection = knex(config);

Depois disso podemos partir para os testes integration, porém será necessário configurar um pacote recomendado para chamadas em API para testes, assim como o axiosno frontend.

f) Instalando supertest

$ npm install supertest -D

Agora podemos importá-lo dentro do arquivo de testes:

const request = require('supertest');

→ O problema do index.js

Para executar o teste deveremos chamar a aplicação para então testarmos suas rotas, porém, há algo que não pode ocorrer enquanto fazemos isso. Todos os testes devem estar isolados da aplicação em si, tendo como principal função apenas testar e não subir a aplicação, por isso, funções como app.listen(3333); não podem estar disponíveis aos testes.

O melhor a ser feito em uma situação como essa é separar em dois módulos isolando dos testes as partes que sobem a aplicação:

  • Renomear index.js para app.js

  • Criar um arquivo chamado server.js dentro da pasta src

      /project
      └── /src
      		├── /contollers
          ├── app.js
      +   ├── server.js
          ├── routes.js
          ├── /database
          ├── /tests
          └── /utils
    
      const app = require('./app');
      
      app.listen(3333);
    
  • Exportar o app.js como app

      module.exports = app;
    
  • Mudar o script de inicialização de index.js para server.js

      "scripts": {
          "start": "nodemon src/server.js",
          "test": "cross-env NODE_ENV=test jest"
        },
    

Dessa forma, o supertest poderá acessar a aplicação sem subir ela, tornando os testes mais seguros.

→ Testando a criação das ONGs (Rota /ongs)

Agora podemos criar os testes dentro da ONG sem se preocupar com nenhuma alteração na aplicação.

Dentro de ong.spec.js é necessário a importação do supertest e app:

const request = require('supertest');
const app = require('../../src/app');

Para descrever o teste devemos criar uma arrow function assíncrona que irá acionar o app() e logo após fazer a requisição .post():

describe('ONG', () => {
    it('should be able to create a new ONG', async () => {
        const response = await request(app).post('/ongs').send();
    });
});

Dentro de .send() podemos fazer uma desestruturação e inserir um corpo de requisição ideal da aplicação:

describe('ONG', () => {
    it('should be able to create a new ONG', async () => {
        const response = await request(app)
            .post('/ongs')
            .send({
								name: "Cachorros e CIA",
								email: "nicolasm15@hotmail.com.br",
								whatsapp: "3511111111",
								city: "Rio de Janeiro",
								uf: "RJ"
            });
            console.log(response.body);
    });
});

Se executarmos os testes, ele irá fazer os testes normalmente porém ele ainda não executou o banco de dados de testes, para isso devemos importar a conexão ao banco de dados e abrir e logo após os testes serem executados, ele deverá fechar o banco de dados.

Caso o banco de dados não feche ele irá informar um warning parecido com este que estará informando que mesmo após as rotinas serem executadas algo continuou executando:

A worker process has failed to exit gracefully and has been force exited. This 
is likely caused by tests leaking due to improper teardown. Try running with --runInBand --detectOpenHandles to find leaks.

A conexão com o banco será dessa forma:

const connection = require('../../src/database/connection');

Para executar a criação da tabela devemos chama as migrations com beforeEach()e afterAll():

describe('ONG', () => {
    beforeEach(async () => {
        await connection.migrate.latest();
    });
    afterAll(async () => {
       await connection.destroy();
    });
(...)

Por fim, o teste de integração está pronto para ser executado.

g) Testando rota de integração

Para testar a rota de integração basta rodar o comando:

$ npm test

3) Deploy

a) Alternativas

Cloud Application Platform | Heroku

DigitalOcean - The developer cloud

b) Qual escolher?

4) Estudos daqui pra frente

a) Padrões de código: ESLint, Prettier

b) Autenticação JWT

c) Styled Components

5) Dicas para aproveitar melhor

a) Github

b) Linkedin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment