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
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"];
for (const order of orders) {
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.
Top comments (0)