Um júnior e um teste técnico: The battle.

Clinton Rocha - Mar 3 - - Dev Community

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

gatinho em cima de um notebook

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

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:

Primeiros passos

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.

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;
Enter fullscreen mode Exit fullscreen mode

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

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;
}
Enter fullscreen mode Exit fullscreen mode

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

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:
async userById(id: number) {
    const user = await this.userRepository.userById(id);
    if (!user) {
      throw new InvalidUser("Usuario invalido");
    }
    return user;
  }
Enter fullscreen mode Exit fullscreen mode
  • 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:
//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;
  }
  //...
}
Enter fullscreen mode Exit fullscreen mode

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. 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 dorepository.
  • 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

No Javascript o 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.

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 });
};
Enter fullscreen mode Exit fullscreen mode

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.

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");
}
Enter fullscreen mode Exit fullscreen mode

Em outro artigo/post que fiz falo mais sobre esse assunto do try-catch e middleware de erros: Lidando com exceções.

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:

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:

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);
  });
Enter fullscreen mode Exit fullscreen mode

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.

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"
    );
  });
Enter fullscreen mode Exit fullscreen mode

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

gatinho digitando no notebook e falando que esta finalizando algo

Após finalizar boa parte desse artigo, mostrei o rascunho para a galera da comunidade He4rt Developers 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 e também o repositório do desafio original. Você pode me encontrar no Twitter ou no Discord da He4rt Developers.

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.

. . . . .