Filosofia
Construindo um framework de testes em Python: como aprimoramos nossos testes por meio da padronização


Desenvolvedor de segurança
5 min
O pytest é o rei das ferramentas de teste em Python. Com mais de 12.000 estrelas no GitHub, uma comunidade muito ativa, aprimoramentos contínuos por meio de novas versões, além de inúmeros forks e plugins que expandem suas funcionalidades, o pytest é a principal referência quando precisamos testar código em Python.
Ao pesquisar por ferramentas ou frameworks de teste em Python na internet, você encontrará artigos como Python Testing Frameworks", de E. Sales; "10 Best Python Testing Frameworks", do GeeksForGeeks; "Top 9 Python Testing Frameworks", de M. Echout; e diversas outras publicações que apresentam listas nas quais o pytest está sempre presente.
Compará-lo a outras ferramentas pode ser injusto, já que o pytest é voltado para testes unitários e de integração, enquanto outros frameworks atuam em domínios muito específicos. O Lettuce e o Behave, por exemplo, introduzem o desenvolvimento orientado a comportamento (Behavior-Driven Development — BDD), mas não são adequados para todas as equipes de desenvolvimento. O Robot é voltado para testes de ponta a ponta (end-to-end) e RPA, mas não se presta a testes unitários ou de integração simples. Já projetos como Testify e TestProject, entre outros, encontram-se abandonados. O módulo unittest (nativo do Python) e o nose2 poderiam ser considerados os principais concorrentes do pytest, mas carecem do suporte, da comunidade e da vasta gama de plugins que o pytest oferece.
Na Fluid Attacks, utilizamos e recomendamos o pytest. Ainda assim, optamos por criar uma camada de abstração (wrapper) sobre ele antes de integrá-lo à nossa base de código. Neste artigo, explico por que você deveria experimentá-lo e como desenvolvemos um framework de testes próprio, focado em garantir que os testes sejam altamente legíveis e fáceis de manter.
Testando com pytest
Um artigo anterior no blog, "From Flaky to Bulletproof" ("De instável a à prova de falhas"), de D. Salazar, orienta nossa intenção de aprimorar os testes unitários e de integração. Nele, recomendamos o uso de um módulo de teste (um wrapper para o pytest) por um motivo fundamental: a padronização.
Quando sua equipe de desenvolvimento é grande, é necessário definir regras para que todos falem a mesma língua. É possível utilizar linters e formatadores para padronizar a sintaxe do código, mas a forma como os programadores escrevem o código é algo mais difícil de padronizar.
Algumas bibliotecas são consideradas "opinativas" (opinionated), pois impõem um uso padronizado, exigindo estruturas de arquivos específicas e definindo seus próprios métodos e classes. Embora o pytest possua seus próprios métodos e estruturas de arquivos, ele é tão flexível que acaba parecendo "não opinativo" (unopinionated).
Algumas das características do pytest — que, dada a sua flexibilidade, podem ser indesejadas — incluem:
O pytest oferece uma forma oficial de realizar mocks (simulações) de métodos e classes, mas você também pode utilizar outras bibliotecas (como a unittest) para esse fim, sem que ocorram conflitos.
As fixtures do pytest são, na prática, uma espécie de "mágica". Elas permitem modificar o comportamento de todos os testes sem a necessidade de referenciar diretamente as funções, evitando, assim, comprometer o fluxo esperado dos testes.
Você pode organizar os testes onde bem entender — inclusive ao lado do código funcional.
É possível utilizar serviços reais ou simulados (mocks), uma vez que a execução do pytest não ocorre em um ambiente isolado (sandbox).
Se você montar uma equipe em expansão, na qual cada integrante possui uma experiência diferente com o pytest, corre o risco de acumular uma enorme dívida técnica, um código ilegível e um aumento exponencial no número de "WTFs por minuto" (expressões de perplexidade diante do código).

Do OSNews.
Por isso, decidimos implementar nosso wrapper para o pytest, que inclui outras bibliotecas de teste — como moto, freezegun ou coverage-py — para que o código de teste seja escrito de uma única maneira.
Vamos falar sobre nossas novas diretrizes e seus benefícios:
Proibição do Pytest

Graças à biblioteca importlinter, proibimos o uso de pytest e unittest em qualquer local que não seja o nosso framework de testes. Evitamos o uso indevido ao restringir o acesso a fixtures do pytest, mocks do unittest e quaisquer outros recursos desnecessários que possam ser introduzidos futuramente. Dessa forma, apenas o módulo de testes pode utilizar o pytest e exportar as ferramentas essenciais para uso.
Utilitários encapsulados
pytest.raises (para capturar exceções), pytest.mark.parametrize (para lidar com múltiplos casos em um mesmo teste) e freezegun.freeze_time (para simular um momento específico no tempo durante a execução de um teste) são os recursos que mais utilizamos. Encontramos uma maneira de encapsulá-los em funções que podem ser importadas a partir do nosso módulo de testes, facilitando sua utilização e permitindo documentar exemplos de como implementá-los.

Serviços simulados
A plataforma da Fluid Attacks utiliza serviços da AWS. Decidimos simulá-los (fazer mock) com a ferramenta moto para obter testes simples e rápidos, graças à simulação em memória de serviços como DynamoDB e S3. No entanto, o moto exige a inclusão de um código repetitivo (boilerplate) para sua execução e para garantir o isolamento entre os testes. Os desenvolvedores poderiam acabar inserindo lógicas complexas nos testes para simular esses serviços, ou esquecer a forma correta de realizar a limpeza (cleanup) dessas simulações — algo que pode ocorrer facilmente ao copiar e colar trechos de código.
Por esse motivo, incluímos um decorator em nosso módulo de testes para inicializar o ambiente AWS simulado. Também encapsulamos todo o processo de inicialização e limpeza, garantindo o isolamento dos testes e simplificando sua estrutura. Os desenvolvedores precisam apenas consultar a documentação para saber como pré-carregar dados ou arquivos para os testes, utilizando uma abordagem declarativa padrão.

Fakers para objetos
De dados Deixando de lado a discussão sobre as diferenças entre fakers, mocks, stubs e spies, precisávamos criar dados fictícios. Resolvemos isso implementando Fakers, uma coleção de funções projetadas para retornar objetos simulados específicos com base nos tipos de dados. Os fakers são simples de implementar e podem chamar outros fakers para preencher campos aninhados. As pessoas desenvolvedoras podem modificar qualquer campo ao chamá-los para maior flexibilidade. Essa abordagem reduz significativamente o código repetitivo (boilerplate), permitindo a criação de objetos de teste bem estruturados sem a necessidade de definir cada propriedade manualmente.
Cobertura por módulo Nos aprofundamos na biblioteca coverage-py para alcançar uma solução modular. Essa ferramenta poderosa gera relatórios detalhados sobre a cobertura de testes para o código executado, servindo como um recurso essencial para identificar lacunas em nossa suíte de testes. Ao analisar esses relatórios, podemos verificar áreas onde testes adicionais são necessários, e a abordagem modular ajuda a focar a equipe de desenvolvimento nos testes mais importantes primeiro. Além disso, a nova estrutura de arquivos fala por si mesma. Os arquivos de teste ficam ao lado dos arquivos normais, oferecendo uma maneira direta de verificar testes ausentes e expandi-los.
Discussão contínua Se algum caso exigir um dos recursos ausentes do pytest, qualquer desenvolvedor ou desenvolvedora pode iniciar uma discussão para validar se uma nova funcionalidade é necessária ou se as ferramentas atuais conseguem lidar com o cenário. Estamos abertos a discussões e melhorias com toda a equipe, priorizando a testabilidade e a legibilidade. Qualquer componente deve ser facilmente testável, e qualquer teste deve ser altamente legível. Adotamos uma solução de testes declarativa (decoradores explícitos na inicialização) e descritiva (testes divididos pelas seções Arrange, Act e Assert).
Resultados
Nosso framework de testes permite que os desenvolvedores verifiquem se um usuário consegue criar uma nova organização (um conjunto de grupos ou projetos a serem avaliados) em nossa plataforma:

Eles também podem verificar se um arquivo foi enviado para o Amazon S3:

Foi uma mudança incrível, pois a quantidade de "WTFs" por minuto que os testes antigos conseguiam gerar era considerável — ainda mais quando era preciso realizar a manutenção desses testes, já que alguns mocks podiam acabar escondendo possíveis bugs:

Assim, obtemos um código muito simples e legível com nosso framework de testes. Os desenvolvedores aprovaram bastante a nova experiência de testes e têm comentado frequentemente sobre a facilidade, a rapidez e a confiança com que executam os novos testes. Essa experiência positiva reduz progressivamente o problema dos "testes sem asserções" (assertion-free tests) e motiva os desenvolvedores a escrever testes realmente importantes. Chegamos a dedicar algumas semanas à migração de testes antigos para o novo framework, em um formato de hackathon, priorizando a qualidade do nosso código para ganharmos agilidade futuramente.
⚠️ Nota: V. Khorikov, em Unit Testing: Principles, Practices, and Patterns, denomina "testes sem asserções" (assertion-free tests) aqueles que não verificam nada. Mesmo que contenham asserções ao final, um teste pode acabar verificando aspectos irrelevantes (por exemplo, checar se uma função foi chamada n vezes, em vez de verificar se uma nova entidade foi, de fato, adicionada ao banco de dados).
A prática de testes contínuos constitui a primeira camada de qualquer estratégia de segurança.
Neste artigo, compartilhei o que aprendemos ao criar um wrapper para o pytest visando a padronização, bem como os grandes benefícios que isso trouxe. Lembre-se: promover a padronização, restringir as funcionalidades ao essencial, utilizar mocks apenas para serviços externos e manter-se aberto a discussões e feedbacks da equipe são atitudes que podem aprimorar a cultura de testes da sua organização. Quanto mais testável e legível for o seu código, mais rapidamente você conseguirá entregar valor (e maior será a sua confiança ao realizar releases em produção).
Agradecimentos especiais
D. Salazar, pelo apoio, pelas discussões e pela implementação do núcleo desta solução comigo.
D. Betancur e J. Restrepo, por incluírem o desenvolvimento do framework de testes no roadmap e priorizarem essa iniciativa.
À equipe de desenvolvimento, por utilizar o framework, oferecer feedbacks valiosos e contribuir para a sua manutenção e evolução.
Comece a usar a solução ASPM da Fluid Attacks agora mesmo
Tags:
cibersegurança
empresa
software
devsecops
Assine nossa newsletter
Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.
Outros posts























