Um júnior e um teste técnico: The battle.
Tags: javascript, webdev, braziliandevs, programming
Published: 2024-03-03 18:35:04

Alguns dias atrás me deparei com um teste técnico e decidi fazer mesmo após já ter finalizado o período de entrega, não sei bem para que nível foi passado o teste, mas acredito que foi usado em diferentes níveis, o que vai definir a complexidade do projeto é a solução do candidato, ao ver isso, decidi fazer aplicando o que venho aprendendo.

Eu nunca fiz teste técnico, muito menos uma entrevista técnica, então neste artigo pretendo explicar minha solução, motivações, reflexões e opiniões.

# Table of contents
- [Objetivo proposto](#objetivo-proposto)
- [Ferramentas escolhidas](#ferramentas-escolhidas)
- [Primeiros passos](#primeiros-passos)
- [Arquitetura e padrão de projeto](#arquitetura-e-padrão-de-projeto)
- [Tratamento de erros](#tratamento-de-erros)
- [Testes](#testes)
- [Duvida](#duvida)
- [Questionamentos](#questionamentos)
- [Conclusão](#conclusão)

# Objetivo proposto 

<img alt="gatinho em cima de um notebook" src="https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExdGJnc2dzMDF6eWo2ZWtpdDA3aDU4c3F2dndteDNzOGRoNHE4dmlhNCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3oKIPnAiaMCws8nOsE/giphy.gif"/>

Em resumo, o desafio é fazer uma API, um sistema de banco simplificado, nessa API existe dois tipos de usuário, o comum e o lojista, o comum pode transferir dinheiro para o lojista ou outro usuário comum, já o lojista não pode fazer transferência, ele apenas receber, veja mais alguns requisitos:

- Validar se o usuário tem saldo antes da transferência.
- Antes de finalizar a transferência, deve-se consultar um serviço autorizador externo, e para isso eles disponibilizaram um `mock` para simular esse serviço.
- A operação de transferência deve ser uma transação (ou seja, revertida em qualquer caso de inconsistência) e o dinheiro deve voltar para a carteira do usuário que envia.
- No recebimento de pagamento, o usuário ou lojista precisa receber notificação (envio de email, sms) enviada por um serviço de terceiro e eventualmente esse serviço pode estar indisponível/instável. Para isso também foi disponibilizado um `mock` para simular o envio.

> `Mock`: É um objeto simulado que imita o comportamento de um componente real. É usado principalmente em testes de software para isolar o código em teste de suas dependências externas, garantindo testes mais rápidos e previsíveis.

O teste técnico não avalia:

- Fluxo de cadastro de usuários e lojistas
- Autenticação

Mas em desencargo de consciência vou explicar o que poderia ser usado, na autenticação poderia ser usado um JWT e para validar os campos da requisição existem diversas estratégias, geralmente eu uso `express-validator` como middleware ou o `class-validator` como seus `decorators`, e podendo ser validado na camada de controller ou também como middleware.

# Ferramentas escolhidas

<img src="https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ25ocDB6OTE4azJmemh1cndzNGZuYzh4N2RwaGpuYmhuYmgyN203ZiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/11JTxkrmq4bGE0/giphy.gif"/>

Para começar com chave de ouro nesse teste técnico, nada mais, nada menos do que o amigo da galera, sim, ele mesmo, nosso tão amado `Javascript`, na verdade, vou usar o `Typescript` junto ao `Express`. Como é a linguagem que tenho uma boa base, a sintaxe não seria mais um problema nesse projeto, me permitindo focar em arquitetura e estrutura do código. As ferramentas que usei foram:

- [Express JS](https://expressjs.com)
- [Typescript](https://www.typescriptlang.org)
- [PostgreSQL](https://www.postgresql.org)
- [Docker | Compose](https://www.docker.com)
- [Vitest](https://vitest.dev)
- [Supertest](https://www.npmjs.com/package/supertest)

# Primeiros passos

<img src="https://media1.giphy.com/media/il2pe9NcggoRa/200.webp?cid=790b76117z920hfqfhw798aj4rabpqu605ntpt3jguyyfzms&ep=v1_gifs_search&rid=200.webp&ct=g"/>

Acredito que não exista uma ordem perfeita para se começar um projeto, já me deparei com pessoas começando pelas rotas, criando estrutura de pastas primeiro que tudo, outras criando os testes para depois implementar as funcionalidades, sendo essa ultima talvez a mais famosa e mais indicada que vejo pela internet, entre outras abordagens, enfim, não sei qual é melhor, após instalar as ferramentas escolhidas e tripagem comecei a desenvolver a tabela do banco.

Confesso que modelagem ainda não é meu forte, e por isso deixei essas duas tabelas o mais simples possível. 

```postgresql
BEGIN;
      CREATE TABLE IF NOT EXISTS users (
        id SERIAL PRIMARY KEY,
        firstName VARCHAR(255) NOT NULL,
        lastName VARCHAR(255) NOT NULL,
        document VARCHAR(14) UNIQUE NOT NULL,
        email VARCHAR(255) UNIQUE NOT NULL,
        password VARCHAR(255) NOT NULL,
        balance INT,
        userType VARCHAR(7) NOT NULL
    );

      CREATE TABLE IF NOT EXISTS transactions (
        id SERIAL PRIMARY KEY,
        payer INT NOT NULL,
        payee INT NOT NULL,
        amount INT NOT NULL,
        date_transaction TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (payer) REFERENCES users(id),
        FOREIGN KEY (payee) REFERENCES users(id)
      );
COMMIT;
```

Tabelas prontas, agora eu posso criar os `types` que vão existir dentro da minha aplicação.  

```ts
type User = {
  id?: number;
  firstName: string;
  lastName: string;
  document: string;
  password: string;
  email: string;
  balance: number;
  usertype: "comum" | "lojista";
};

type Transaction = {
  id?: number;
  amount: number;
  payer: number;
  payee: number;
  date_transaction?: Date;
}
```

Após perguntas para alguns amigos e pesquisar como geralmente é feito uma modelagem desse tipo, levantei essas duas possíveis melhorias: 

- Fazer mais uma tabela para guardar os cargos, imagine um cenário onde poderiam existir outros cargos do usuário, em vez de ter direto na tabela `user`, poderia salvar esses cargos em outra tabela e relaciona ao usuário, resultado em uma normalização dos dados. 

- Ter outra tabela onde eu salvaria o saldo da conta do cliente, assim eu não precisaria ficar atualizando a tabela do usuário toda vez que for feita uma transferência.

# Arquitetura e padrão de projeto

![arquitetura em camadas escolhida para o projeto](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uwy0olqw1n4cfowtej5q.png)

Acabei optando por utilizar uma arquitetura em camadas. Pelo que venho estudando, essa estratégia é bastante interessante por alguns motivos: 
- Desacoplamento de código. 
- Separação de responsabilidade. 
- Modularidade e reutilização de código. 
- Facilidade na hora de criar testes. 

Não posso deixar de citar os pontos negativos: 
- Complexidade adicional. 
- Dificuldade de implementação no início do projeto. 

Além disso, foi usado alguns princípios da orientação a objetos, sim, o famoso SOLID. No meu código você pode encontrar princípios como: 

- `Responsabilidade única`, onde exemplo é o seguinte método que se comunica com a camada de repositório e procura um usuário pelo id:

```typescript
async userById(id: number) {
    const user = await this.userRepository.userById(id);
    if (!user) {
      throw new InvalidUser("Usuario invalido");
    }
    return user;
  }
```

- `Inversão da dependência` e `aberto-fechado`: usando a interface como uma abstração do banco de dados, o `UserService` não vai mais precisar saber qual o banco minha aplicação está usando:

```typescript
//abstração
export interface IUserRepository {
  newUser(user: User): Promise<User>;
  userById(id: number): Promise<User>;
  emailExist(email: string): Promise<boolean>;
  documentExist(document: string): Promise<boolean>;
}

export class UserService {
  constructor(private userRepository: IUserRepository) {}
  
  async newUser(user: User) {
 
    const EmailExist = await this.userRepository.emailExist(user.email);
    
    if (!EmailExist) {
      throw new InvalidEmail("Email invalido");
    }
    
    const DocumentExist = await this.userRepository.documentExist(
      user.document
    );

    if (!DocumentExist) {
      throw new InvalidDocument("Document invalid");
    }

    const newUser = await this.userRepository.newUser(user);
    
    return newUser;
  }
  //...
}
```

 A meu ver, o código fica muito mais limpo, só se deve ter cuidado com a quantidade de abstração, o uso excessivo pode acarreta um emaranhado de código. 
 
 >Em outro artigo/post explico melhor sobre SOLID, nele eu trago alguns exemplos em código e imagens que podem te ajudar a entender melhor sobre esses princípios: [Era SOLID o que me faltava](https://dev.to/clintonrocha98/era-solid-o-que-me-faltava-bhp). Agora entrando na parte de organização de projeto, vou explicar um pouco das funções de cada camada, o projeto tem três camadas, são elas: 
 
 - `Repository`: camada responsável por interagir com o banco de dados. 
 - `Service`: camada onde a lógica de negócio acontece, interações com outros serviços e manipulação dos dados enviados/recebidos do`repository`. 
 - `Controller`: camada onde recebemos o corpo da requisição do cliente, podendo ser feito validações, verificações nas informações recebidas e enviadas, também podendo chamar diferentes serviços.

# Tratamento de erros

<img src="https://media3.giphy.com/media/kELWH7yHTEWlrOrgli/giphy.webp?cid=790b76117z920hfqfhw798aj4rabpqu605ntpt3jguyyfzms&ep=v1_gifs_search&rid=giphy.webp&ct=g"/>

No `Javascript` o [`try catch`](https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/Statements/try...catch) é uma estrutura usada para lidar com exceções durante a execução de código, mas isso gera uma repetição de estrutura tremenda no código, fora que faz com que uma funcionalidade possa ter diferentes tipos de retorno, acaba deixando o código desorganizado e muito difícil de se entender. 

Tendo esse detalhe da linguagem em mente, optei pelo uso de middleware de erros e uma pequena biblioteca `express-async-errors` fazendo com que minhas exceções não saiam de controle.

``` typescript
export const errorMiddleware = (
  error: Error & Partial<ApiError>,
  request: Request,
  response: Response,
  next: NextFunction
) => {
  const statusCode = error.statusCode ?? 500;
  const message = error.statusCode ? error.message : "Internal Server Error";

  return response.status(statusCode).json({ error: message, code: statusCode });
};
```

Com o uso desse middleware eu posso lançar exceções e não quebrar a aplicação e nem mostrar erros tenebrosos para o usuário, mas não para por aqui, para deixar os erros ainda mais fáceis de se ler, eu fiz uma pequena personalização.

```typescript
export class ApiError extends Error {
  public readonly statusCode: number;

  constructor(message: string, statusCode: number) {
    super(message);
    this.statusCode = statusCode;
  }
}

// Um dos erros que usei na aplicação 
export class UnauthorizedTransaction extends ApiError {
  constructor(message: string) {
    super(message, 401);
  }

}

//Caso de uso.
const isAuthorized = await this.authorizationService.authorizeTransaction();

if (!isAuthorized) {
  throw new UnauthorizedTransaction("Transação não autorizada");
}
```

>Em outro artigo/post que fiz falo mais sobre esse assunto do `try-catch` e middleware de erros: [Lidando com exceções](https://dev.to/clintonrocha98/lidando-com-excecoes-como-fazer-erros-praticos-e-elegantes-no-express-js-4k5b).

# Testes

Esse projeto me fez melhor muito na maneira que desenvolvo os meus testes e a estrutura escolhida para esse projeto auxiliou muito para isso, mais alguns benefícios que a estrutura do projeto proporcionou:  

- Me permite criar mocks em camadas especificas
- [In Memory Test Database](https://martinfowler.com/bliki/InMemoryTestDatabase.html)

Os `mocks` são interessantes de se usar, eu pensava que poderia sair colocando `mock` em tudo, mas não é bem assim que funciona, como qualquer coisa na tecnologia, toda ferramenta tem seu lugar na atuação, onde eu acredito que ele brilhe é em pontos específicos do teste, por exemplo:

```typescript
test("authorizeTransaction should return false when authorization is not successful", async () => {
    global.fetch = vi.fn().mockResolvedValue({
      json: () => Promise.resolve({ message: "Not authorized" }),
    });
    
    const result = await authService.authorizeTransaction();

    expect(result).toBe(false);
  });
```

Esse é um exemplo onde o `mock` me permitiu testa o retorno de algo que não tenho controle, no caso seria uma API de autorização de transação.

```typescript
test("It should not be possible to create a user with an email that already exists", async () => {
    const { fakeUser, userService } = makeSut();
    await userService.newUser(fakeUser);

    await expect(userService.newUser(fakeUser)).rejects.toThrow(
      "Email invalido"
    );
  });
```

O `In Memory Database` é uma abordagem que aprendi faz pouco tempo, no link que disponibilizei fala mais sobre os benefícios do uso dessa estratégia, mas no caso dessa minha aplicação, o ponto de destaque foi os testes onde eu esperava retornos de falha, sem o uso de banco de dados eu consigo testa de maneiras simples e rápida pontos de falhas. 

Nos testes E2E foram usados o `supertest` em conjunto de um container Docker rodando um banco de dados postgreSQL, para que não tivesse problema entre esses testes E2E foi usado um pequeno script para limpar o banco.

# Duvida

A maioria das vezes as soluções que pensei nesse desafio e também em outros projetos que já fiz, são consideradas como `overengineering`, e pelo que vejo pela internet, provavelmente quase todo júnior na hora do desenvolvimento de alguma aplicação já passou por isso. Tendo isso em mente, eu me questionei, como que esse `overengineering` seria visto pelo entrevistador? Eu estaria mostrando conhecimento ou iria parecer esta copiando ''modelo'' de solução? Provavelmente o que responderia essas perguntas seria minha entrevista.

> `Overengineering`: É quando você cria uma solução mais complexa ou robusta do que realmente precisa. É como usar um canhão para matar uma mosca. Isso pode resultar em desperdício de tempo, recursos e dificuldade de manutenção. Em resumo, é quando você torna algo mais complicado do que é necessário para resolver o problema em questão.

# Questionamentos

<img alt="gatinho digitando no notebook e falando que esta finalizando algo" src="https://media.giphy.com/media/5AGvUbf5yBaUcRDkkc/giphy.gif?cid=790b7611hatnpcd973kgaqpfbtoaj8k0qqp4w5slec6lidom&ep=v1_gifs_search&rid=giphy.gif&ct=g"/>

Após finalizar boa parte desse artigo, mostrei o rascunho para a galera da comunidade [He4rt Developers](https://twitter.com/He4rtDevs) resultando em um bate-papo bem legal, nele eu fui levado a questionar ainda mais as escolhas em relação às tecnologias usadas no processo de desenvolvimento. Eu vou esta fugindo um pouco do tema principal desse artigo, mas esses questionamentos são necessários para todo desenvolvedor e acredito que vão fazer você evoluir de alguma maneira.

Usar ou não usar um framework? Convenhamos que usar determinado framework iria facilitar e muito o processo de desenvolvimento, mas temos que entender uma coisa, um framework é ferramenta que se encaixa em situações especificas e resolvem problemas específicos, e não para por aí, o desenvolvedor tem que entender o contexto do problema que ele vai solucionar, existem limitações e requisitos referente a estrutura, tem toda uma análise antes de se escolher uma tecnologia. O mesmo questionamento vale para linguagens.

Após levantar esses pontos, quero mostrar questionamentos que normalmente se deve ter antes de escolher uma tecnologia:

- Qual a quantidade de usuários que vai utilizar? 
- O que está disponível para a utilização na infraestrutura da empresa?
- Terá novas implementações dentro desta lógica? 
- Qual a criticidade do processo? 
- Qual o tempo ideal de resposta para cada rota? 

Você já tinha se perguntado isso alguma vez antes? Pelo que vejo de outros desenvolvedores mais experientes, esses questionamentos fazem total diferença.

# Conclusão

Como foi dito no início, eu nunca tinha feito um teste técnico antes, acredito que após de ter feito essa solução e ao fazer esse artigo, me fez pensar em muita coisa em relação a tudo isso, provavelmente eu não teria a mesma postura que tive nesse projeto, mas de qualquer forma eu saí aprendendo algo.

E você leitor, aprendeu algo com esse artigo? Tem algum feedback? Se tiver alguma explicação errada nesse artigo, eu ficaria grato com sua correção. Será que eu iria passar nesse teste? Fica essa dúvida no ar. 

Quase que esqueço, caso você queira ver minha solução por completo, ela está no meu [Github](https://github.com/Clintonrocha98/picpay-desafio-backend) e também o repositório do [desafio original](https://github.com/PicPay/picpay-desafio-backend/tree/master?tab=readme-ov-file#objetivo-picpay-simplificado). Você pode me encontrar no [Twitter](https://twitter.com/ClintonRocha_) ou no Discord da [He4rt Developers](https://twitter.com/He4rtDevs). 

Gostaria de agradecer ao @carloseduardodb e a @cherryramatis pela revisão desse conteúdo, feedbacks e dicas. Os questionamentos foram levantados pelo Carlos e eu achei de grande ajuda, para esse artigo e ainda mais para mim. 

"Eu tentaria fazer uma solução mais próxima da realidade do pessoal de lá para ganhar uns pontinhos. Às vezes, nem é utilizar a mesma linguagem que eles, mas entender os problemas que eles têm e como eles solucionam hoje."
>@carloseduardodb 

Fica aí mais uma dica.

Obrigado por ler até aqui.