Estratégias para escrever código com maior testabilidade – uma análise imperativa

Código com maior testabilidade

Observações Iniciais

A melhora na escrita de testes e na testabilidade do código depende da maturidade da equipe com:

O ambiente de desenvolvimento
O desenvolvimento de testes
A arquitetura do si…


This content originally appeared on DEV Community and was authored by Felipe Leao

Código com maior testabilidade

Observações Iniciais

  • A melhora na escrita de testes e na testabilidade do código depende da maturidade da equipe com:

    • O ambiente de desenvolvimento
    • O desenvolvimento de testes
    • A arquitetura do sistema
    • A compreensão e clareza de requisitos
  • Os pontos tratados nesse documento possuem base no estudo teórico e no conhecimento adquirido ao longo da prática exaustiva do desenvolvimento de testes:

    • Tais práticas serão úteis e relevantes, no entanto, padrões devem ser criados e mantidos pelas equipes para que o workflow de desenvolver código e escrever testes vire um fluxo comum e de fácil adaptação para todos.

Para que servem os testes?

  • Testes servem como documentação para outros desenvolvedores:

    • Facilita o entendimento e a manutenção dos produtos de uma empresa.
  • Testes transmitem confiança para desenvolvimento e manutenção de fluxos de produto.

  • Testes indicam se o seu código está bem escrito:

    • Se é difícil escrever testes para um código, ele pode estar mal escrito, complexo, verboso, acoplado demais, ou mais complicado do que poderia.
  • Testes detectam falhas durante a etapa de desenvolvimento:

    • Evitam que essas falhas sejam encontradas pelos usuários.

Por que ter um código bem testável é positivo para a vida útil de um software?

  • O fluxo de um software sempre muda para acompanhar o mundo real e as necessidades dos usuários:

    • Manter a cultura de testes de uma equipe depende da relação tempo/benefício em escrever testes.
  • Ter um código bem testável implica na adoção e manutenção de uma estrutura/arquitetura de codebase, gerando:

    • Fácil entendimento e desenvolvimento
    • Redução do tempo para encontrar falhas
    • Melhor entendimento de fluxos complexos
    • Fluxo de trabalho menos custoso, cansativo e desorganizado

Estratégias para melhorar a testabilidade de um código

  • entendimento completo de requisitos e do design de um sistema

    • isso inclui a infra da qual dispõe o código
    • a forma como os componentes internos e externos interagem
  • Padronização de erros ( forma como ele será retornado )

    • padronização de mensagens de erro
  • Definição de 'breakpoints'.

  • Padronização de erros ( Molde )

    • padronização de mensagens de erro
  • Normalização de dados

  • Separação de componentes internos e externos bem desacoplada

Destrinchemos os pontos acima:

Infraestrutura Disponível

Entender a infraestrutura envolve conhecer os recursos disponíveis, como servidores, serviços em nuvem, bancos de dados, e como o código será executado. Isso afeta decisões de design, tomadas de decisão no desenvolvimento e cenários de testes.

Interação entre Componentes Internos e Externos

Conhecer como os componentes internos (módulos, serviços) e externos (APIs, bancos de dados, sistemas de terceiros) interagem é crucial para a tomada de decisão, o design do sistema e os cenários de testes. Por exemplo, ao integrar com uma API de terceiros, é importante definir claramente como lidar com falhas ou latências para manter a robustez do sistema. Em um segundo ponto, manter componentes internos bem desacoplados de componentes externos
nos ajuda a criar mocks, simular possibilidades e ter o controle de todos os cenários possiveís na feature
que estamos desenvolvendo.

Padronização de erros e mensagem de erros

Por que padronizar erros e mensagem de erros?

  • Consistência: Garantia de uso da mesma terminologia em todo o sistema.
  • Facilidade de Manutenção: Alterações feitas em um único lugar, sem precisar buscar e substituir em todo o código.
  • Internacionalização: Facilita a tradução ao centralizar as mensagens e instancia-las apenas uma vez.
  • Testabilidade: Mensagens previsíveis facilitam a validação de exceções em testes.
  • Reutilização: Mensagens de erro podem ser usadas de forma uniforme em diferentes partes da aplicação.
export const createErrors = (error_message: string, status_code: number) => {
  const error = new Error(error_message);

  return {
    error: error,
    status_code,
  };
};

export const ErrorMessages = {
  INVALID_PASSWORD: "Invalid password",
  USER_ALREADY_EXISTS: "User already exists",
} as const;

export const createUser = async ({
  email,
  password,
}: {
  email: string;
  password: string;
}) => {
  const validPassword = validatePassword(password);

  if (!validPassword) {
    return createErrors(ErrorMessages.INVALID_PASSWORD, 422); // breakpoint
  }

  const userExists = await findUserByEmail(email);

  if (userExists) {
    return createErrors(ErrorMessages.USER_ALREADY_EXISTS, 412); //breakpoint
  }
};

Normalização de dados

O que é normalização de dados?

  • Normalização de dados é o processo de transformar dados não estruturados para um formato consistente e estruturado antes de usá-los no restante do sistema. Isso ajuda a garantir que o sistema funcione de maneira previsível, consistente e desacoplado de componentes externos. Isso serve para qualquer componente externo, Fonte de cache, fonte de dados, mensageria, storage...

Por que normalizar dados?

  • Separação de responsabilidades: Decisões podem ser tomadas somente para componentes externos, a aplicação é tratada como uma entidade independente.
  • Testabilidade: Gera tipos e interfaces totalmente ligadas a aplicação, facilitando a previsibilidade e mock de resultados.
  • Documentação: A normalização cria uma documentação implícita do formato esperado de dados.
const orders = await db.order.findMany();

// [
//   {
//     "id": 1,
//     "customer_id": 101,
//     "product_id": 202,
//     "quantity": 2,
//     "total_price": 59.99,
//     "created_at": "...",
//     "status": "shipped",
//     "delivery_date": "...",
//     "notes": "FRÁGIL"
//   },
//   ...
// ]

type NormalizedOrder = {
  orderId: number;
  customerId: number;
  productId: number;
  quantity: number;
  totalPrice: number;
  status: string;
  deliveryDate: string | null;
  notes?: string;
};

// normalizando generalizando
function normalizeOrders(orders: any[]): NormalizedOrder[] {
  return orders.map((order) => ({
    orderId: order.id,
    customerId: order.customer_id,
    productId: order.product_id,
    quantity: order.quantity,
    totalPrice: Number(order.total_price),
    status: order.status,
    deliveryDate: order.delivery_date
      ? new Date(order.delivery_date).toISOString()
      : null,
    notes: order.notes,
  }));
}

// normalizando por adapter

import { Order as PrismaOrder } from "@prisma/client";
import { Order as MongoOrder } from "mongodb";

function normalizePrismaOrder(order: PrismaOrder[]): NormalizedOrder {
  return {
    orderId: order.id,
    customerId: order.customer_id,
    productId: order.product_id,
    quantity: order.quantity,
    totalPrice: Number(order.total_price),
    status: order.status,
    deliveryDate: order.delivery_date
      ? new Date(order.delivery_date).toISOString()
      : null,
    notes: order.notes,
  };
}

function normalizeOrmOrder(order: MongoOrder[]): NormalizedOrder {
  return {
    orderId: order._id,
    customerId: order.customerId,
    productId: order.productId,
    quantity: order.quantity,
    totalPrice: order.totalPrice,
    status: order.status,
    deliveryDate: order.deliveryDate ? order.deliveryDate.toISOString() : null,
    notes: order.notes,
  };
}

Esteira de instruções que não carregam lógicas complexas demais.

import client as mailClient from "@sendgrid/mail";
import { db } from "./db";
import dotenv from 'dotenv'

dotenv.config()

const processOrder = async(data: {
  order,
  user_id
}, {
  order: {
    quantity: number;
    item: string
  }[],
  user_id: string
}) => {


  const user = await db.user.findUnique({
    where: {id: user_id}
  })
  if (order.quantity <= 0) {
    console.log("Invalid quantity");
    return;
  }

  const validItems = ["Laptop", "Smartphone", "Tablet"];
  if (!validItems.includes(order.item)) {
    console.log("Invalid item");
    return;
  }

  const message = {
    from: "store@gmail.com",
    to: user.email,
    subject: "Compra realizada",
    body: `corpo do email`,
  };

  const mailClient = mailClient.setApiKey(process.env.SENDGRID_KEY);

  const data = await client.send(message);

  return {ok: true}
}


import client as mailClient from "@mail";
import { db } from "./db";
import dotenv from 'dotenv'

dotenv.config()


const ErrorMessages = {
  INVALID_QUANTITY: "Invalid quantity",
  INVALID_ITEM: "Invalid item",
  USER_NOT_FOUND: "User not found",
  MAIL_NOT_SENT: "Mail not sent",
} as const; // mensagem de erros instanciadas em uma única fonte


const getUserById = async(id: number) => {
  const user = await db.user.findUnique({
    where: { id }
  })

  if (!user) {
    console.log(ErrorMessages.USER_NOT_FOUND) // mensagem de erro padronizada
    return null
  }

  return user;
}

const validateOrder = (order: {
  quantity: number;
  item: string;
}) => {
  if (order.quantity <= 0) {
    console.log(ErrorMessages.INVALID_QUANTITY); // mensagem de erro padronizada
    return false;
  }

  const validItems = ["Laptop", "Smartphone", "Tablet"];
  if (!validItems.includes(order.item)) {
    console.log(ErrorMessages.INVALID_ITEM);
    return false;
  }

  return true;
}

const sendOrderRequestedMail = async (email_to: string) => {
  const message = {
    from: "store@gmail.com",
    to: email_to,
    subject: "Compra realizada",
    body: "corpo do email",
  }

  const mailClient = mailClient.setApiKey(process.env.MAIL_CLIENT_KEY);


  const mailSent = await client.send(message);

  if(!mailSent) {
    console.log(ErrorMessages.MAIL_NOT_SENT)
    return { ok: false }
  }
}

const processOrder = async({
  orders,
  user_id
  }: {
  orders: {
    quantity: number;
    item: string;
  }[],
  user_id: number;
  } ) => {

  const user = await getUserById(user_id); // desacoplamento do db

  if (!user) {
    return {ok: false} // breakpoint
   }



  for( const order of orders) {
    if (!validateOrder(order)) {
      return {ok: false} // breakpoint
    }
  }

  await sendOrderRequestedMail(user.email); // desacoplamento do mail

  return { ok: true }
}

Beneficios da refatoração

  • Flexibilidade : Com uma arquitetura modular e mensagens de erro padronizadas, é mais fácil adicionar novas funcionalidades e fazer alterações no código sem impactar outras partes do sistema.

  • Reusabilidade: As funções podem ser usadas em diferentes contextos(ex: getUserById)

  • Testes: Dividir o codigo desta maneira desencadea uma facilidade em criar mocks, stubs e spies e cenários de testes completos com uma esteira simples e clara, ademais, permite o teste em pequenos escopos da esteira, que são basicamente as causas dos breakpoints.


This content originally appeared on DEV Community and was authored by Felipe Leao


Print Share Comment Cite Upload Translate Updates
APA

Felipe Leao | Sciencx (2024-08-31T00:09:17+00:00) Estratégias para escrever código com maior testabilidade – uma análise imperativa. Retrieved from https://www.scien.cx/2024/08/31/estrategias-para-escrever-codigo-com-maior-testabilidade-uma-analise-imperativa/

MLA
" » Estratégias para escrever código com maior testabilidade – uma análise imperativa." Felipe Leao | Sciencx - Saturday August 31, 2024, https://www.scien.cx/2024/08/31/estrategias-para-escrever-codigo-com-maior-testabilidade-uma-analise-imperativa/
HARVARD
Felipe Leao | Sciencx Saturday August 31, 2024 » Estratégias para escrever código com maior testabilidade – uma análise imperativa., viewed ,<https://www.scien.cx/2024/08/31/estrategias-para-escrever-codigo-com-maior-testabilidade-uma-analise-imperativa/>
VANCOUVER
Felipe Leao | Sciencx - » Estratégias para escrever código com maior testabilidade – uma análise imperativa. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2024/08/31/estrategias-para-escrever-codigo-com-maior-testabilidade-uma-analise-imperativa/
CHICAGO
" » Estratégias para escrever código com maior testabilidade – uma análise imperativa." Felipe Leao | Sciencx - Accessed . https://www.scien.cx/2024/08/31/estrategias-para-escrever-codigo-com-maior-testabilidade-uma-analise-imperativa/
IEEE
" » Estratégias para escrever código com maior testabilidade – uma análise imperativa." Felipe Leao | Sciencx [Online]. Available: https://www.scien.cx/2024/08/31/estrategias-para-escrever-codigo-com-maior-testabilidade-uma-analise-imperativa/. [Accessed: ]
rf:citation
» Estratégias para escrever código com maior testabilidade – uma análise imperativa | Felipe Leao | Sciencx | https://www.scien.cx/2024/08/31/estrategias-para-escrever-codigo-com-maior-testabilidade-uma-analise-imperativa/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.