Joando qualquer nova empresa – com uma cultura e práticas de programação estabelecidas – pode ser uma experiência assustadora. Quando me juntei à equipe da Ansible, decidi escrever as práticas e princípios de engenharia de software que aprendi ao longo dos anos e com os quais me esforço para trabalhar. Esta é uma lista não definitiva e não exaustiva de princípios que devem ser aplicados com sabedoria e flexibilidade.
A minha paixão é pelos testes, pois acredito que boas práticas de testes podem tanto garantir um padrão mínimo de qualidade (infelizmente faltando em muitos produtos de software), como podem orientar e moldar o próprio desenvolvimento. Muitos destes princípios estão relacionados a práticas e ideais de teste. Alguns destes princípios são específicos do Python, mas a maioria não o são. (Para desenvolvedores Python, PEP 8 deve ser sua primeira parada para estilo de programação e diretrizes.)
Em geral, nós programadores somos muito opinativos, e opiniões fortes são freqüentemente um sinal de grande paixão. Com isso em mente, sinta-se livre para discordar desses pontos, e podemos discuti-los e debatê-los nos comentários.
Desenvolvimento e teste das melhores práticas
1. YAGNI: “Você não vai precisar disso”. Não escreva código que você acha que pode precisar no futuro, mas não precisa ainda. Isto é codificação para casos imaginários de uso futuro, e inevitavelmente o código se tornará código morto ou precisará ser reescrito porque o caso de uso futuro sempre funciona um pouco diferente de como você o imaginou.
Se você colocar código para um caso de uso futuro, eu o questionarei em uma revisão de código. (Você pode, e deve, desenhar APIs, por exemplo, para permitir casos de uso futuro, mas isso é um problema diferente.)
O mesmo é verdade para comentar código; se um bloco de código comentado vai para uma release, ele não deve existir. Se é código que pode ser restaurado, faça um ticket e faça referência ao hash de commit para o código excluir. YAGNI é um elemento central da programação ágil. A melhor referência para isso é Extreme Programming Explained, de Kent Beck.
2. Os testes não precisam ser testados. Infra-estrutura, frameworks e bibliotecas para testes precisam de testes. Não teste o navegador ou bibliotecas externas, a menos que você realmente precise. Teste o código que você escreve, não o código de outras pessoas.
3. A terceira vez que você escreve o mesmo código é o momento certo para extraí-lo em um helper de propósito geral (e escrever testes para ele). Funções de helper dentro de um teste não precisam de testes; quando você as quebra e as reutiliza, elas precisam de testes. Pela terceira vez que você escreve código similar, você tende a ter uma idéia clara do formato do problema de propósito geral que você está resolvendo.
4. Quando se trata de design de API (external face and object API): Coisas simples devem ser simples; coisas complexas devem ser possíveis. Desenho para o caso simples primeiro, de preferência com configuração ou parametrização zero, se isso for possível. Adicionar opções ou métodos API adicionais para casos mais complexos e flexíveis (conforme necessário).
5. Falha rápida. Verifique a entrada e falhe na entrada sem sentido ou estado inválido o mais cedo possível, de preferência com uma resposta de exceção ou erro que deixará o problema exato claro para o seu interlocutor. Permita casos “inovadores” de uso do seu código (isto é, não faça verificação de tipo para validação de entrada a menos que você realmente precise).
6. Teste de unidade para a unidade de comportamento, não para a unidade de implementação. Mudar a implementação, sem mudar o comportamento ou ter que mudar qualquer um dos seus testes é o objetivo, embora nem sempre seja possível. Portanto, sempre que possível, trate seus objetos de teste como caixas pretas, testando através da API pública sem chamar métodos privados ou mexer no estado.
Para alguns cenários complexos – como testar o comportamento em um estado complexo específico para encontrar um bug obscuro – isso pode não ser possível. Escrever testes primeiro realmente ajuda com isso, pois obriga você a pensar sobre o comportamento do seu código e como você vai testá-lo antes de escrevê-lo. Testar primeiro encoraja unidades de código menores e mais modulares, o que geralmente significa um código melhor. Uma boa referência para começar com a abordagem “teste primeiro” é Test Driven Development by Example, de Kent Beck.
7. Para testes de unidades (incluindo testes de infra-estrutura de teste) todos os caminhos de código devem ser testados. 100% de cobertura é um bom lugar para começar. Você não pode cobrir todas as possíveis permutações/combinações de estado (explosão combinatória), o que requer consideração. Somente se houver uma razão muito boa é que os caminhos de código devem ser deixados por testar. A falta de tempo não é um bom motivo e acaba por custar mais tempo. Possíveis boas razões incluem: genuinamente não testado (de qualquer forma significativa), impossível de bater na prática, ou coberto em outro lugar em um teste. O código sem testes é uma responsabilidade. Medir a cobertura e rejeitar PRs que reduzam a percentagem de cobertura é uma forma de assegurar um progresso gradual na direcção certa.
8. O código é o inimigo: pode correr mal, e precisa de manutenção. Escreva menos código. Elimine o código. Não escreva código que você não precisa.
9. Inevitavelmente, os comentários de código tornam-se mentiras ao longo do tempo. Na prática, poucas pessoas atualizam os comentários quando as coisas mudam. Esforce-se para tornar seu código legível e autodocumentado através de boas práticas de nomenclatura e estilo de programação conhecido.
Código que não pode ser tornado óbvio – trabalhar em torno de um bug obscuro ou condição improvável, ou uma otimização necessária – precisa ser comentado. Comente a intenção do código, e porque ele está fazendo algo ao invés do que ele está fazendo. (Este ponto em particular sobre comentários serem mentiras é controverso, a propósito. Eu ainda acho correto, e Kernighan e Pike, autores de The Practice of Programming, concordam comigo.)
10. Escreva de forma defensiva. Pense sempre no que pode dar errado, o que acontecerá em entradas inválidas e o que pode falhar, o que o ajudará a pegar muitos bugs antes que eles aconteçam.
11. A lógica é fácil de testar por unidade se for sem estado e sem efeitos colaterais. Divida a lógica em funções separadas, ao invés de misturar a lógica em código cheio de estados e de efeitos colaterais. Separar código stateful e código com efeitos colaterais em funções menores torna-os mais fáceis de zombar e testar por unidade sem efeitos colaterais. (Menos sobrecarga para testes significa testes mais rápidos.) Efeitos colaterais precisam ser testados, mas testá-los uma vez e zombar deles em qualquer outro lugar geralmente é um bom padrão.
12. Globais são ruins. As funções são melhores que os tipos. Objetos provavelmente são melhores que estruturas de dados complexas.
13. Usando os tipos incorporados em Python – e seus métodos – será mais rápido que escrever seus próprios tipos (a menos que você esteja escrevendo em C). Se a performance é uma consideração, tente trabalhar como usar os tipos padrão incorporados ao invés de objetos customizados.
14. A injeção de dependência é um padrão de codificação útil para ser claro sobre quais são suas dependências e de onde elas vêm. (Tenha objetos, métodos, e assim por diante receba suas dependências como parâmetros ao invés de instanciar novos objetos em si). Isto torna as assinaturas de API mais complexas, por isso é uma troca. Acabar com um método que precisa de 10 parâmetros para todas as suas dependências é um bom sinal de que seu código está fazendo muito, de qualquer forma. O artigo definitivo sobre injeção de dependência é “Inversão de Containers de Controle e o Padrão de Injeção de Dependência”, de Martin Fowler.
15. Quanto mais você tem que gozar para testar seu código, pior é seu código. Quanto mais código você tiver que instanciar e colocar em prática para poder testar um determinado comportamento, pior é o seu código. O objetivo é pequenas unidades testáveis, juntamente com integração de alto nível e testes funcionais para testar se as unidades cooperam corretamente.
16. APIs de face externa são onde “projetar de frente” – e consideração sobre casos de uso futuro – realmente importa. Mudar as APIs é uma dor para nós e para nossos usuários, e criar incompatibilidades ao contrário é horrível (embora às vezes impossível de evitar). Projetar APIs de face externa cuidadosamente, mantendo o princípio “coisas simples devem ser simples”.
17. Se uma função ou método passa de 30 linhas de código, considere quebrá-lo. Um bom tamanho máximo de módulo é de cerca de 500 linhas. Os arquivos de teste tendem a ser mais longos que isto.
18. Não trabalhe em construtores de objetos, que são difíceis de testar e surpreendentes. Não coloque código em __init__.py (exceto importações para namespacing). __init__.py não é onde os programadores geralmente esperam encontrar código, por isso é “surpreendente”
19. DRY (Don’t Repeat Yourself) é muito menos importante nos testes do que no código de produção. A legibilidade de um arquivo de teste individual é mais importante do que a manutenção (quebrando pedaços reutilizáveis). Isso porque os testes são executados e lidos individualmente em vez de serem eles próprios parte de um sistema maior. Obviamente repetição excessiva significa que componentes reutilizáveis podem ser criados por conveniência, mas é muito menos preocupante do que para a produção.
20. Refator sempre que você vê a necessidade e tem a chance. A programação é sobre abstrações, e quanto mais próximo o seu mapa de abstrações do domínio do problema, mais fácil o seu código é de entender e manter. Conforme os sistemas crescem organicamente, eles precisam mudar a estrutura para seu caso de uso em expansão. Os sistemas superam suas abstrações e estrutura, e não mudá-las torna-se uma dívida técnica que é mais dolorosa (e mais lenta e mais buggy) para trabalhar ao redor. Inclua o custo da compensação da dívida técnica (refactoring) nas estimativas para o trabalho de recursos. Quanto mais tempo se deixa a dívida em torno, mais altos são os juros acumulados. Um grande livro sobre refactoring e testes é Working Effectively with Legacy Code, de Michael Feathers.
21. Faça o código correto primeiro e rápido segundo. Ao trabalhar em questões de performance, faça sempre o perfil antes de fazer correções. Normalmente o gargalo não está exatamente onde você pensava que estava. Escrever código obscuro porque é mais rápido só vale a pena se você fez um perfil e provou que realmente vale a pena. Escrever um teste que exercita o código que você está traçando com o tempo em torno dele torna mais fácil saber quando você está pronto, e pode ser deixado na suíte de testes para evitar regressões de desempenho. (Com a nota usual de que adicionar código de tempo sempre altera as características de performance do código, fazendo com que a performance funcione uma das tarefas mais frustrantes.)
22. Testes unitários menores e mais apertados dão informações mais valiosas quando falham – eles dizem a você especificamente o que está errado. Um teste que resiste a metade do sistema para testar o comportamento, requer mais investigação para determinar o que está errado. Geralmente um teste que leva mais de 0,1 segundos para ser executado não é um teste unitário. Não existe tal coisa como um teste de unidade lento. Com um comportamento de teste de unidade bem definido, os seus testes funcionam como uma especificação de facto para o seu código. Idealmente se alguém quiser entender o seu código, deve ser capaz de recorrer ao conjunto de testes como “documentação” do comportamento. Uma ótima apresentação sobre as práticas de testes unitários é Fast Test, Slow Test, de Gary Bernhardt:
23. “Não Inventado Aqui” não é tão ruim quanto as pessoas dizem. Se escrevermos o código, então sabemos o que ele faz, sabemos como mantê-lo, e somos livres para estendê-lo e modificá-lo como quisermos. Isto segue o princípio YAGNI: Temos código específico para os casos de uso que precisamos, em vez de código de propósito geral que tem complexidade para coisas que não precisamos. Por outro lado, o código é o inimigo, e possuir mais código do que o necessário é mau. Considere o trade-off ao introduzir uma nova dependência.
24. A propriedade compartilhada do código é o objetivo; o conhecimento em silo é ruim. No mínimo, isto significa discutir ou documentar decisões de projeto e decisões importantes de implementação. A revisão de código é o pior momento para começar a discutir as decisões de projeto, já que a inércia para fazer mudanças radicais após o código ter sido escrito é difícil de superar. (Claro que ainda é melhor apontar e alterar erros de projeto no momento da revisão do que nunca.)
25. Os geradores são o máximo! Eles são geralmente mais curtos e fáceis de entender do que objetos de estado para iteração ou execução repetida. Uma boa introdução aos geradores é “Generator Tricks for Systems Programmers”, por David Beazley.
26. Vamos ser engenheiros! Vamos pensar em projetar e construir sistemas robustos e bem implementados, ao invés de crescer monstros orgânicos. A programação é um ato de equilíbrio, no entanto. Nem sempre estamos construindo um foguete espacial. A sobre-engenharia (arquitetura cebola) é tão dolorosa de se trabalhar como o código sub-projetado. Quase qualquer coisa de Robert Martin vale a pena ler, e Arquitetura Limpa: Um Guia do Artesão para Estrutura e Design de Software é um bom recurso sobre este tópico. Design Patterns é um livro clássico de programação que todo engenheiro deve ler.
27. Testes com falhas intermitentes corroem o valor da sua suíte de testes, a ponto de, eventualmente, todos ignorarem os resultados dos testes porque sempre há algo falhando. Corrigir ou apagar testes com falha intermitente é doloroso, mas vale o esforço.
28. Geralmente, particularmente nos testes, espere por uma mudança específica ao invés de dormir por um período de tempo arbitrário. Dormir vodu é difícil de entender e retarda o seu conjunto de testes.
29. Veja sempre o seu teste falhar pelo menos uma vez. Coloque um bug deliberado e certifique-se de que ele falha, ou execute o teste antes que o comportamento sob teste esteja completo. Caso contrário, você não sabe que está realmente testando alguma coisa. Escrever acidentalmente testes que realmente não testam nada ou que nunca podem falhar é fácil.
30. E finalmente, um ponto para a gerência: O desenvolvimento de software é uma forma terrível de desenvolver software. Não deixar os desenvolvedores se orgulharem de seu trabalho garante que você não vai obter o melhor deles. Não abordar a dívida técnica atrasa o desenvolvimento e resulta num produto pior e mais buggy.