Este artigo foi publicado pela primeira vez no Blog do TK.
Isso faz parte dos meus estudos sobre como criar software sustentável e consistente. Neste post, falaremos sobre o pensamento por trás do desenvolvimento orientado a testes e como aplicar esse conhecimento a funções simples, acessibilidade à Web e componentes do React, principalmente com o Jest e o React Testing Library.
Testes automatizados são uma grande parte do desenvolvimento de software. Isso nos dá, desenvolvedores, confiança para enviar o código para estar lá, mas aumentamos a confiança de que o software estará em funcionamento e funcionando adequadamente.
Comecei minha carreira de software na comunidade Ruby escrevendo testes desde o primeiro dia em que aprendi o idioma. A comunidade Ruby (e Rails) sempre foi forte na área de automação de testes. Isso ajudou a moldar minha mentalidade sobre como escrever um bom software.
Então, usando Ruby e Rails, fiz várias coisas de back-end, como trabalhos em segundo plano, modelagem de estrutura de dados, criação de API e assim por diante. Nesse escopo, o usuário é sempre um: o usuário desenvolvedor. Ao criar uma API, o usuário seria o desenvolvedor que está consumindo a API. Ao criar os modelos, o usuário seria o desenvolvedor que usará esse modelo.
Agora, fazendo um monte de coisas de front-end também, após 1 ano intenso de construção de PWAs usando principalmente o React e o Redux, a princípio alguns pensamentos vieram à minha mente:
- TDD é impossível ao criar coisas de interface do usuário. Como sei se é um div ou span?
- O teste pode ser “complexo”. Devo raso ou devo montar? Testar tudo? Garantir que todas as div sejam o lugar certo?
Então comecei a repensar essas práticas de teste e como torná-lo produtivo.
TDD é possível. Se estou me perguntando se devo esperar um div ou um span, provavelmente estou testando a coisa errada. Lembre-se: os testes devem nos dar a confiança necessária para enviar, não necessariamente para cobrir todos os detalhes da implementação. Vamos abordar esse tópico mais tarde!
Eu quero criar testes que:
- Verifique se o software funciona adequadamente
- Confie no envio do código à produção
- Faça-nos pensar em design de software
E testes que produzem software:
- De fácil manutenção
- Fácil de refatorar
Testando o desenvolvimento orientado
TDD não deve ser complexo. É apenas um processo de 3 etapas:
- Faça um teste
- Faça correr
- Faça certo
Começamos a escrever um teste simples para cobrir como esperamos que o software funcione. Em seguida, fazemos a primeira implementação do código (classe, função, script, etc). Agora o software está se comportando. Funciona como esperado. Hora de acertar. Hora de melhorar.
O objetivo é um código limpo que funcione. Resolvemos o problema “que funciona” primeiro e depois limpamos o código.
É bem simples. E deveria ser. Eu não disse que é fácil. Mas é simples, direto, apenas 3 etapas. Toda vez que você exercita esse processo de escrever testes primeiro, codificar depois e depois refatorar, você se sente mais confiante.
Uma boa técnica ao escrever seus testes primeiro é pensar nos casos de uso e simular como eles devem ser usados (como uma função, componente ou usado por um usuário real).
Funções
Vamos aplicar essa coisa TDD em funções simples.
Há algum tempo, eu estava implementando um recurso preliminar para um fluxo de registro de imóveis. Parte do recurso era mostrar um modal se o usuário não tivesse um imóvel finalizado. A função que implementaremos é aquela que responde se o usuário tiver pelo menos um rascunho imobiliário.
Então, primeiro passo: escrevendo o teste! Vamos pensar nos casos de uso dessa função. Ele sempre responde um booleano: verdadeiro ou falso.
- Não possui minuta imobiliária não salva:
false
- Tem pelo menos um rascunho de imóvel não salvo:
true
Vamos escrever os testes que representam esse comportamento:
describe('hasRealEstateDraft', () => {
describe('with real estate drafts', () => {
it('returns true', () => {
const realEstateDrafts = [
{
address: 'São Paulo',
status: 'UNSAVED'
}
];
expect(hasRealEstateDraft(realEstateDrafts)).toBeTruthy();
});
});
describe('with not drafts', () => {
it('returns false', () => {
expect(hasRealEstateDraft([])).toBeFalsy();
});
});
});
Nós escrevemos os testes. Mas, ao executá-lo, ele fica vermelho: 2 testes quebrados porque ainda não temos a função implementada.
Segundo passo: faça funcionar! Nesse caso, é bem simples. Precisamos receber esse objeto de matriz e retornar se ele possui ou não pelo menos um rascunho imobiliário.
const hasRealEstateDraft = (realEstateDrafts) => realEstateDrafts.length > 0;
Ótimo! Função simples. Testes simples. Poderíamos ir para o passo 3: faça a coisa certa! Mas, neste caso, nossa função é realmente simples e já a acertamos.
Mas agora precisamos da função para obter os rascunhos de imóveis e passá-los para o hasRealEstateDraft
.
Em que caso de uso podemos pensar?
- Uma lista vazia de imóveis
- Apenas imóveis salvos
- Apenas imóveis não salvos
- Misto: salvar e não salvar imóveis
Vamos escrever os testes para representá-lo:
describe('getRealEstateDrafts', () => {
describe('with an empty list', () => {
it('returns an empty list', () => {
const realEstates = [];
expect(getRealEstateDrafts(realEstates)).toMatchObject([]);
});
});
describe('with only unsaved real estates', () => {
it('returns the drafts', () => {
const realEstates = [
{
address: 'São Paulo',
status: 'UNSAVED'
},
{
address: 'Tokyo',
status: 'UNSAVED'
}
];
expect(getRealEstateDrafts(realEstates)).toMatchObject(realEstates);
});
});
describe('with only saved real estates', () => {
it('returns an empty list', () => {
const realEstates = [
{
address: 'São Paulo',
status: 'SAVED'
},
{
address: 'Tokyo',
status: 'SAVED'
}
];
expect(getRealEstateDrafts(realEstates)).toMatchObject([]);
});
});
describe('with saved and unsaved real estates', () => {
it('returns the drafts', () => {
const realEstates = [
{
address: 'São Paulo',
status: 'SAVED'
},
{
address: 'Tokyo',
status: 'UNSAVED'
}
];
expect(getRealEstateDrafts(realEstates)).toMatchObject([{
address: 'Tokyo',
status: 'UNSAVED'
}]);
});
});
});
Ótimo! Nós executamos os testes. Não funciona .. ainda! Agora implemente a função.
const getRealEstatesDrafts = (realEstates) => {
const unsavedRealEstates = realEstates.filter((realEstate) => realEstate.status === 'UNSAVED');
return unsavedRealEstates;
};
Simplesmente filtramos pelo status do imóvel e devolvemos. Ótimo, os testes estão passando, a barra é verde! E o software está se comportando, mas podemos melhorar: etapa 3!
Que tal extrair a função anônima dentro do diretório filter
função e faça o 'UNSAVED'
ser representado por um enum?
const STATUS = {
UNSAVED: 'UNSAVED',
SAVED: 'SAVED',
};
const byUnsaved = (realEstate) => realEstate.status === STATUS.UNSAVED;
const getRealEstatesDrafts = (realEstates) => realEstates.filter(byUnsaved);
Os testes ainda estão passando e temos uma solução melhor.
Uma coisa a ter em mente aqui: eu isolei a fonte de dados da lógica. O que isso significa? Obtemos os dados do armazenamento local (fonte de dados), mas testamos apenas as funções responsáveis pela lógica para obter rascunhos e ver se há pelo menos um rascunho. As funções com a lógica, garantimos que funcione e seja um código limpo.
Se conseguirmos o localStorage
dentro de nossas funções, fica difícil testar. Portanto, separamos a responsabilidade e tornamos os testes fáceis de escrever. As funções puras são mais fáceis de manter e mais simples para escrever testes.
Reagir componentes
Agora vamos falar sobre os componentes do React. De volta à introdução, falamos sobre escrever testes que testam detalhes de implementação. E agora veremos como podemos torná-lo melhor, mais sustentável e ter mais confiança.
Há alguns dias, eu estava planejando construir as novas informações de integração para o proprietário do imóvel. É basicamente um monte de páginas com o mesmo design, mas altera o ícone, o título e a descrição das páginas.

Eu queria criar apenas um componente: Content
e passe as informações necessárias para renderizar o ícone, título e descrição corretos. Eu passaria businessContext
e step
como adereços e renderizaria o conteúdo correto para a página de integração.
Não queremos saber se renderizaremos uma tag div ou paragraph. Nosso teste precisa garantir que, para um determinado contexto e etapa de negócios, o conteúdo correto esteja lá. Então, eu vim com estes casos de uso:
- A primeira etapa do contexto de negócios de aluguel
- Última etapa do contexto de negócios de aluguel
- A primeira etapa do contexto de negócios de vendas
- Última etapa do contexto de negócios de vendas
Vamos ver os testes:
describe('Content', () => {
describe('in the rental context', () => {
const defaultProps = {
businessContext: BUSINESS_CONTEXT.RENTAL
};
it('renders the title and description for the first step', () => {
const step = 0;
const { getByText } = render( );
expect(getByText('the first step title')).toBeInTheDocument();
expect(getByText('the first step description')).toBeInTheDocument();
});
it('renders the title and description for the forth step', () => {
const step = 3;
const { getByText } = render( );
expect(getByText('the last step title')).toBeInTheDocument();
expect(getByText('the last step description')).toBeInTheDocument();
});
});
describe('in the sales context', () => {
const defaultProps = {
businessContext: BUSINESS_CONTEXT.SALE
};
it('renders the title and description for the first step', () => {
const step = 0;
const { getByText } = render( );
expect(getByText('the first step title')).toBeInTheDocument();
expect(getByText('the first step description')).toBeInTheDocument();
});
it('renders the title and description for the last step', () => {
const step = 6;
const { getByText } = render( );
expect(getByText('the last step title')).toBeInTheDocument();
expect(getByText('the last step description')).toBeInTheDocument();
});
});
});
Nós temos um describe
bloco para cada contexto de negócios e um it
bloco para cada etapa. Também criei um teste de acessibilidade para garantir que o componente que estamos construindo esteja acessível.
it('has not accessibility violations', async () => {
const props = {
businessContext: BUSINESS_CONTEXT.SALE,
step: 0,
};
const { container } = render( );
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Agora precisamos fazê-lo funcionar! Basicamente, a parte da interface do usuário deste componente é apenas o ícone, o título e a descrição. Algo como:
{title}
{description}
Nós apenas precisamos construir a lógica para obter todos esses dados corretos. Como eu tenho o businessContext
e a step
neste componente, eu queria apenas fazer algo como
content[businessContext][step]
E obtém o conteúdo correto. Então, construí uma estrutura de dados para funcionar dessa maneira.
const onboardingStepsContent = {
alugar: {
0: {
Icon: Home,
title: 'first step title',
description: 'first step description',
},
// ...
},
vender: {
0: {
Icon: Home,
title: 'first step title',
description: 'first step description',
},
// ...
},
};
É apenas um objeto com as primeiras chaves como dados do contexto de negócios e, para cada contexto de negócios, possui chaves que representam cada etapa da integração. E nosso componente seria:
const Content = ({ businessContext, step }) => {
const onboardingStepsContent = {
alugar: {
0: {
Icon: Home,
title: 'first step title',
description: 'first step description',
},
// ...
},
vender: {
0: {
Icon: Home,
title: 'first step title',
description: 'first step description',
},
// ...
},
};
const { Icon, title, description } = onboardingStepsContent[businessContext][step];
return (
{title}
{description}
);
};
Funciona! Agora vamos melhorar. Eu queria tornar o conteúdo mais resistente. E se receber uma etapa que não existe, por exemplo? Estes são os casos de uso:
- A primeira etapa do contexto de negócios de aluguel
- Última etapa do contexto de negócios de aluguel
- A primeira etapa do contexto de negócios de vendas
- Última etapa do contexto de negócios de vendas
- Etapa inexistente do contexto de negócios de aluguel
- Etapa inexistente do contexto de negócios de vendas
Vamos ver os testes:
describe('getOnboardingStepContent', () => {
describe('when it receives existent businessContext and step', () => {
it('returns the correct content for the step in "alugar" businessContext', () => {
const businessContext = 'alugar';
const step = 0;
expect(getOnboardingStepContent({ businessContext, step })).toMatchObject({
Icon: Home,
title: 'first step title',
description: 'first step description',
});
});
it('returns the correct content for the step in "vender" businessContext', () => {
const businessContext = 'vender';
const step = 5;
expect(getOnboardingStepContent({ businessContext, step })).toMatchObject({
Icon: ContractSign,
title: 'last step title',
description: 'last step description',
});
});
});
describe('when it receives inexistent step for a given businessContext', () => {
it('returns the first step of "alugar" businessContext', () => {
const businessContext = 'alugar';
const step = 7;
expect(getOnboardingStepContent({ businessContext, step })).toMatchObject({
Icon: Home,
title: 'first step title',
description: 'first step description',
});
});
it('returns the first step of "vender" businessContext', () => {
const businessContext = 'vender';
const step = 10;
expect(getOnboardingStepContent({ businessContext, step })).toMatchObject({
Icon: Home,
title: 'first step title',
description: 'first step description',
});
});
});
});
Ótimo! Agora vamos construir nossa getOnboardingStepContent
função para lidar com essa lógica.
const getOnboardingStepContent = ({ businessContext, step }) => {
const content = onboardingStepsContent[businessContext][step];
return content
? content
: onboardingStepsContent[businessContext][0];
};
Tentamos obter conteúdo. Se tivermos, basta devolvê-lo. Se não tivermos, retorne o primeiro passo da integração.
Arrumado! Mas nós podemos melhorá-lo. Que tal usar o ||
operador? Não há necessidade de atribuir a uma variável, não há necessidade de usar um ternário.
const getOnboardingStepContent = ({ businessContext, step }) =>
onboardingStepsContent[businessContext][step] ||
onboardingStepsContent[businessContext][0];
Se encontrar o conteúdo, basta devolvê-lo. Se não encontrou, retorne a primeira etapa do contexto comercial especificado.
Agora, nosso componente é apenas interface do usuário.
const Content = ({ businessContext, step }) => {
const {
Icon,
title,
description,
} = getOnboardingStepContent({ businessContext, step });
return (
{title}
{description}
);
};
Pensamentos finais
Eu gosto de pensar profundamente nos testes que estou escrevendo. E acho que todos os desenvolvedores deveriam também. Ele precisa nos dar confiança para enviar mais código e ter um impacto maior no mercado em que estamos trabalhando.
Como todo código, quando escrevemos testes malcheirosos e ruins, ele influencia outros desenvolvedores a seguir o “padrão”. Fica pior em empresas maiores. Escala mal. Mas sempre podemos parar, refletir sobre o status quo e agir para torná-lo melhor.
Compartilhei alguns recursos que achei interessantes de leitura e aprendizagem. Se você deseja obter uma ótima introdução ao TDD, eu realmente recomendo o TDD por exemplo, um livro de Kent Beck.
Vou escrever mais sobre testes, TDD e React. E como podemos tornar nosso software mais consistente e nos sentirmos seguros ao enviar o código para produção.