Apresentando a pergunta central do curso
Nesta primeira aula do nosso curso de testes de APIs em Node.js, vamos desenvolver a teoria por trás das provas antes de passar à prática. Embora pudesse ser mais fácil introduzir o tema mostrando como testamos uma API que já desenvolvemos com Jest ou outras ferramentas de teste, queremos construir um repertório para responder com clareza à pergunta que está na tela: por que precisamos testar?
Pode parecer simples responder que precisamos testar para garantir que o sistema funcione. Porém, na Alura, queremos que haja uma base mais sólida e teórica, para explicar por que o que fazemos na nossa profissão é importante e por que fazemos dessa forma. Para isso, pedimos atenção, pois vamos construir esse raciocínio gradualmente. Em cada vídeo, neste e nos próximos, vamos construir uma escada de conhecimento para responder a essa pergunta, tornando muito mais claro por que dedicamos tempo e esforço à escrita de testes para nossos sistemas.
Definindo objetivos do software e caracterizando sistemas complexos
Para começar, vamos abordar quais são os objetivos do software em geral. É algo básico, talvez até óbvio, mas destacamos quatro pontos para iniciar a linha de raciocínio:
- Resolver problemas: pode ser algo simples, como criar um programa que jogue xadrez, resolver uma equação matemática ou calcular um imposto.
- Automatizar processos: processos do mundo real podem ser automatizados para facilitar, padronizar e permitir auditoria.
- Simular a realidade: exemplos incluem softwares de física ou AutoCAD.
- Entretenimento: como jogos.
A partir desses exemplos, vamos separar visualmente dois grupos para entender melhor uma distinção importante. Em um lado, temos um software bancário e uma rede social. O software bancário pode se encaixar tanto em resolução de problemas quanto em automação de processos, enquanto a rede social se aproxima do entretenimento. No outro lado, temos um software de simulação física e um software de logística, que resolve problemas como: se precisamos visitar cinco cidades, qual é a rota mais rápida para visitar todas, possivelmente gastando menos combustível.
Existe uma distinção entre esses dois grupos. Os softwares do primeiro grupo estão mais relacionados à mecanização de atividades humanas. O fator humano está muito presente, pois automatizamos e sistematizamos interações humanas: no sistema bancário, com pessoas no caixa emitindo cheques ou realizando saques; e, ainda mais, na rede social, com interações totalmente entre pessoas.
No segundo grupo, os sistemas não têm, necessariamente, interação humana direta. O valor de um software de simulação física, por exemplo, está associado a simular corretamente fenômenos como aceleração e gravidade. Em logística, a abordagem tende a ser mais heurística: tenta-se resolver um problema de forma eficiente, ainda que não exata, modelando o mundo real com menor influência do fator humano e maior ênfase no componente matemático.
Vamos focar nos sistemas com os quais trabalhamos com mais frequência, tipicamente os do primeiro grupo. Quando você está desenvolvendo em Node.js e evoluindo na carreira, costumamos atuar mais com sistemas desse lado, como os bancários e as redes sociais, que possuem um componente humano mais forte.
Esses sistemas têm uma característica especial: são constantemente influenciados pelo ambiente em que operam. No caso de uma rede social, por exemplo, seu valor está mais ligado à percepção das pessoas usuárias sobre ela do que a um valor intrínseco, como “modelar bem como duas pessoas se tornam amigas na realidade”. Uma rede social é influenciada por como as pessoas a utilizam, pelo contexto cultural e geográfico onde opera (por exemplo, uma rede social no Brasil pode ter comportamentos distintos de uma na Ásia), por nichos de interesse diferentes, por regulações que mudam ao longo do tempo e pelo próprio passar do tempo, com gerações que passam a usar (ou deixam de usar) determinada plataforma, enquanto outra se torna mais relevante.
O ponto central é que esses são sistemas que operam em ambientes fortemente influenciados pelo contexto em que estão inseridos. Chamamos esses sistemas de sistemas complexos porque estão em constante evolução, sofrem influência e também influenciam os atores que interagem com eles.
Ao longo do tempo (no eixo x, o transcurso do tempo), para que sistemas complexos continuem relevantes, precisam mudar conforme certos eventos ocorrem. Por exemplo, surge uma nova legislação no país onde o sistema opera: o sistema precisa ser modificado para continuar em uso. Mais adiante, pode surgir uma nova geração de pessoas usuárias; para atendê-la, novas funcionalidades se fazem necessárias. Assim, esses sistemas estão sempre mudando; precisam mudar para permanecer em operação.
Um professor, o doutor Lima, do Imperial College de Londres, postulou um conjunto de leis que descrevem precisamente o comportamento de sistemas complexos. Há várias leis em seu trabalho, mas vamos nos concentrar em três que são muito relevantes para o nosso contexto:
- Lei da mudança contínua.
- Lei da complexidade crescente.
- Lei da qualidade em declínio.
Esses fundamentos orientarão por que investimos tempo em testes e como estruturamos nossa prática ao testar APIs em Node.js com ferramentas como Jest. Ao longo das próximas aulas, vamos aprofundar cada um desses pontos e conectar a teoria com a prática, passo a passo.
Leis de Lehman e evolução de sistemas complexos
Os nomes talvez pareçam autoexplicativos; vamos revisá-los rapidamente para enfatizar os seguintes pontos:
- Lei da mudança contínua: em sistemas complexos, é necessário mudar continuamente para permanecer relevante. Se não houver mudanças, o sistema rapidamente deixará de ser utilizado.
- Aumento da complexidade: uma consequência das mudanças constantes é o aumento da complexidade. O que é complexidade? É a quantidade de regras, condições e casos excepcionais que existirão na plataforma.
- Qualidade em declínio: com o tempo, devido ao aumento da complexidade e ao custo de manutenção, observamos mais bugs (erros). Em outras palavras, o sistema deixa de funcionar ou passa a funcionar de maneira inesperada.
A partir disso, começamos a entender por que estamos falando de testes e de sua importância.
Como lidar com sistemas complexos em evolução
Dado o contexto do que são sistemas complexos e das leis de Lehman que operam sobre eles, o que fazemos para começar a lidar com esse cenário e garantir que construímos sistemas resilientes, capazes de suportar mudanças e complexidade crescente?
Precisamos nos concentrar em dois pontos:
- Como resolvemos os problemas corretos do sistema.
- Como resolvemos o problema corretamente.
Essa diferenciação pode parecer sutil, então vamos à prática.
Exemplo: sistema de livraria
Vamos utilizar o mesmo exemplo que já vimos em outros cursos: um sistema de livraria. Se alguém não viu os outros cursos, vamos recapitular rapidamente: nosso sistema de livraria é voltado à venda de livros. Registramos livros, que têm um autor e uma editora associados. Temos clientes cadastrados e, quando realizam compras, tratamos essa operação na parte de vendas do sistema.
Dado esse contexto, avaliemos primeiro o caso de “resolver o problema correto”. Considere três funcionalidades durante a fase de concepção do sistema, quando estamos definindo o que implementar:
- Comprar um livro;
- Buscar um livro;
- Apostar um livro.
Quais dessas funcionalidades fazem sentido no sistema? Comprar e buscar um livro fazem sentido; apostar um livro não faz sentido, e incluir uma seção de apostas seria inadequado.
Esse exemplo é propositalmente óbvio. Porém, em sistemas reais, isso ocorre com frequência: equipes de pessoas desenvolvedoras, Product Owners (responsáveis pelo produto), pessoas de produto e demais stakeholders (partes interessadas) reúnem-se para avaliar funcionalidades e, por vezes, identificam equivocadamente um problema que acreditam que o cliente final tem.
Um exemplo mais sutil: imaginemos que pensamos que o cliente final tem o problema de possuir muitos livros em casa e queira vender seus livros usados em nosso sistema. Acabamos implementando isso e, depois, descobrimos que o cliente final não quer essa funcionalidade; frequentemente são colecionadores que usam nosso sistema e desejam manter até mesmo livros antigos.
Esse é o primeiro ponto em sistemas complexos e em evolução: precisamos nos concentrar em resolver os problemas corretos, aqueles que nossos clientes finais realmente têm.
Como resumimos isso de forma prática? Para verificar se estamos resolvendo o problema correto, fazemos duas perguntas:
- Estamos realmente resolvendo um problema real?
- Esse problema faz parte do repertório da nossa empresa?
Isso também é essencial. Muitas vezes, um sistema começa simples, resolvendo uma dor muito específica do cliente final. Com o tempo, tenta abarcar diversos outros problemas que até podem existir, mas não são o foco da empresa. O software (programa) não nasceu com essa proposta; não faz parte da especialidade da empresa resolver aquele problema. No sistema de livraria, por exemplo, poderia ser oferecer crédito para compra de vários livros ao mesmo tempo, algo que talvez não faça parte da nossa especialidade. Portanto, além de ser o problema correto, precisa fazer sentido para a empresa.
Resolver um problema corretamente
Agora, vamos à segunda parte: resolver um problema corretamente. Isso é mais direto.
Foquemos no exemplo de buscar um livro, mais especificamente por título. Precisamos aprofundar o nível de detalhe dessa funcionalidade, isto é, definir sua especificação. O que esperamos ao buscar um livro por título?
Vamos considerar três comportamentos:
- Se o título existir, mostramos um livro na tela.
- Se o título não existir, mostramos uma lista vazia.
- Se a busca utilizar um título vazio, retornamos um erro informando que é necessário fornecer um título com pelo menos um caractere.
Isso é a especificação do sistema, pois define como ele deve se comportar.
Agora, imaginemos que, ao construir um protótipo, verificamos o seguinte: nos dois primeiros casos, o comportamento está conforme o esperado. No terceiro caso, no entanto, onde esperávamos que um título vazio gerasse um erro, o sistema retorna uma lista vazia. Esse não era o comportamento definido; queríamos um erro.
Portanto, especificamos que o sistema deveria funcionar de certa maneira, mas o comportamento observado foi diferente. Resolver o problema corretamente significa garantir o seguinte: o sistema se comporta como estabelecemos que deveria se comportar? Se sim, estamos resolvendo corretamente. Se não, não estamos resolvendo corretamente.
Encerramento e próximos passos
Essa é a primeira parte do nosso desenvolvimento teórico: entendemos o que é um sistema complexo, as leis de Lehman que o regem e em que precisamos nos concentrar.
No próximo vídeo, vamos aprofundar e construir o próximo degrau do nosso conhecimento: o que precisamos fazer daqui para frente? Já entendemos o problema e que precisamos focar em resolver os problemas corretos e resolvê-los corretamente. Agora, como fazemos isso? Veremos no próximo vídeo.