Como desenvolvemos e testamos APIs na Mercos

Israel Fonseca
mercos-engineering
Published in
7 min readAug 3, 2017

Normalmente quando se procura material sobre técnicas de teste de software, caímos em um papo muito teórico ou com exemplos pra cenários bem loucos que parecem não representar o "cenário típico do desenvolvimento de software web brasileiro". Digo isso baseado em praticamente… nada. Mas não importa, o que escreverei agora é algo que teria me ajudado lá no início do meu aprendizado sobre testes, me dando uma melhor noção do big picture então que fique como ajuda para quem precisar. Para isso utilizarei como exemplo um dos nossos endpoints do nosso sistema.

Disclaimer: Você já precisa estar um "pouco ligado" em testes e já ter desenvolvido alguns endpoints REST para absorver melhor esse conteúdo. Se está iniciando agora, recomendo o material do meu grande colega (1,90m ou mais) Jorge Modesto Neto e suas obras sobre teste unitário.

Primeiramente um pouco de contexto. No que diz respeito ao nosso backend Python nós temos basicamente três grandes partes:

  • API Interna: Cuida da sincronização dos nossos mobile devices.
  • API Externa: Cuida da integração com nossos parceiros.
  • Backend Mercos: O clássico Multi Page Application com pitadas de REST para algumas partes específicas do APP.
  • Backend B2B: Toda glória do RESTful API*.

*Aquele REST arte. REST menino. REST moleque. Aquele REST que talvez, só talvez tenha algum endpoint que não seja indepotente ou que carregue algum estado entre requests. Não vamos focar nisso agora, se quer aprender mais sobre REST raíz, recomendo a excelente publicação do Cléber Zavadniak. Mas lembre-se sobre a grande verdade do desenvolvimento de software: para cada escolha, há uma perda.

E por último um pouco de nomenclatura, já que usamos a não tão usual Clean Architecture que é baseada na um pouco mais famosa Hexagonal Architecture

  • usecases são nossas classes de "Regra de Negócio", que não possuem dependências com bibliotecas fora do builtin do python a menos que sejam extremamente importantes. Namorar o framework até vai, mas casar é melhor evitar.
  • entities são nossas funções puras (normalmente) com "Regras de Negócio" que podem ser compartilhadas entre usecases.
  • gateways normalmente acessam o banco de dados, sendo análogos aos DAOs ou Repositories (mais ou menos), mas eles também podem estar conectados a outros serviços como API de terceiros e outras bibliotecas.
  • presenters são os gateways da "tela". Utilizam bibliotecas que vão gerar o output para o cliente do nosso endpoint.

Já desistiu? Não? Então aperte os cintos por que a bocha começa agora.

Meu senso de humor não está bom e daqui pra baixo só piora.

O Problema

Vou simular aqui um passo-a-passo (bem mais ou menos pra não virar uma leitura de 30 minutos) da ordem e as classes utilizadas para implementar uma feature do nosso sistema.

A feature é simples. O cliente do nosso cliente que acessar o sistema deve poder ver uma lista de representadas que ele tem acesso. Isso pode variar de três formas:

  • Vê todas ativas caso o nosso cliente não tenha um plano que permita configurar restrições.
  • Vê todas se o plano permitir restrição, mas o nosso cliente não tiver configurado restrições.
  • Vê apenas as que ele tiver acesso que foi configurado anteriormente pelo nosso cliente.

Primeiro Passo: Teste Integrado*

*Utilizo o termo "Teste Integrado" como o sinônimo de "Teste de Sistema" que por sua vez é um sinônimo de "Teste Funcional" que também tem o apelido de "Teste E2E". Pheew. Se você não concorda com o meu rótulo, concorde pelo menos até o fim do artigo para estarmos falando da mesma coisa. Prometo que farei um novo artigo sobre o complexo mundo de nomenclaturas depois (talvez até mude de ideia com alguns nomes aqui).

Começamos criando um, e apenas um, teste integrado. Por que só um? Porque testes integrados são lentos e tendem a ter um setup difícil. Normalmente é preciso ter uma fixture gigante para deixar o sistema num estado minimamente utilizável. O Django já nos oferece um arquivo JSON que podemos preencher para que ele então inicialize a base com dados para nós pobres desenvolvedores.

Teste Integrado. Não caia na ilusão de achar que ele é tudo que você precisa.

Nesse teste, validamos que um GET no endpoint /representadas/ consegue trazer as representadas do subdomínio especificado no header HTTP_ORIGIN. Também precisamos estar autenticados, o que é feito no setUp do teste.

Feito o teste, vamos para a implementação da nossa view do Django que irá processar a request. Esse é o código que chamamos de "cola", já que ele não tem nenhuma condicional, apenas gruda os envolvidos que realmente fazem alguma coisa.

Com a ausência de condicionais errar alguma coisa aqui é bem difícil.

Destaques do código:

  • @pode_permitir_acesso_publico decorator responsável pelo nosso controle de acesso. Esse componente já havia sido criado e testado unitariamente anteriormente, só usamos. Note que não temos um outro teste integrado que justifique o uso desse decorator. Ao encadear chamadas simples sem condicionais na view Django, não parece razoável que alguém faria algo errado ali. A lentidão de um teste integrado não seria justificada e um teste unitário dessa região seria preciosismo demais.
  • ListarRepresentadasPresenterDjango a classe que gera o output que será devolvido para o cliente, iremos nos aprofundar nela depois.
  • ObterRepresentadasPorClienteUsecaseFactory a factory que constrói nosso usecase. Usamos as factories para esconder um pouco da sujeira da instanciação do nosso controller, além de facilitar os casos esporádicos em que reutilizamos o usecase em outro lugar. As factories não são testadas unitariamente, sendo testadas indiretamente apenas pelos testes integrados.

Segundo Passo: Teste Unitário

Depois da "carcaça" pronta, começamos a implementação do usecase. Assim como no teste integrado, a maioria das nossas soluções são feitas com test first ou TDD (talvez eu disserte mais sobre a diferença em uma futura postagem, mas lembre-se que o Google é seu amigo). Para simplificar nesse e nos próximos exemplos só colocarei um teste:

Utilizamos o unittest builtin do Python e o mock como biblioteca externa.

Os destaques aqui são:

  • criar_mock(Protocolo) prepara os nossos Mocks (nome popular, mas que o correto seria stub) para podermos fazermos o setup do nosso teste. Fizemos um wrapper da biblioteca mock para fazer algumas otimizações, mas basicamente ele está criando um mock que respeita a assinatura dos métodos do protocolo enviado.
  • Nomes super verbosos como test_apresenta_todas_as_representadas_quando_conta_nao_possui_acesso_a_representada_por_cliente para servirem de documentação e nos ajudar a entender melhor o código. Não utilizamos comentários em 99,99% das vezes.

O resto é só o teste propriamente dito, nele garantimos que uma conta cuja a assinatura é de um plano que não possui o recurso de "representada por cliente", não restringe as representadas, mostrando todas as ativas. O código de produção final é este:

Poderíamos matar o problema com uma consulta só, mas isso dificultaria o reuso dos métodos em detrimento de desempenho. Cada escolha uma perda.

Perceba como é importante passar os gateways como dependência no construtor para que possamos alterar a implementação (com o Mock) durante os testes para aumentar o desempenho e facilitar o setup.

Olhos atentos perceberão que há uma dependência hardcoded chamada PermissaoEntity. Não "mockamos" entities, pois a simplicidade delas e a ausência de dependência externas as tornam simples o bastante para serem gerenciadas no teste. Ainda assim, elas também possuem testes unitários separados.

Terceiro Passo: Testes de Integração

Por último temos que garantir que o nosso código que interage com o framework está testado. Não há segredo, mesmo esquema que o teste unitário só que sem mocks agora, apenas utilizando a implementação concreta.

Por exemplo, nesse teste criamos o registro na base excluído e validamos que só o ativo é retornado. Eis a implementação final:

Para o presenter, é semelhante, mas aqui a integração é com a biblioteca de JSON/Reponses do Django:

Sem muitos segredos, aqui só temos um teste validando a estrutura do JSON criado na Response do Django.

A implementação também não possui mistérios.

"Matt o técnico do Radar" pode até ter algumas dificuldades, mas você leitor, certamente não terá.

Conclusão

Divindindo os testes assim conseguimos alguns benefícios:

  • Divisão de Responsabilidades (SOLID) fica bem explícita com "Persistência", "Apresentação" e "Negócio".
  • A Velocidade de execução dos testes é muito alta. Usecases quando rodados individualmente levam milisegundos e não há remorso por ter milhares de testes (temos +2300 que rodam apenas em memória). Os testes de gateway e presenters também são muito rápidos quando executados individualmente.
  • Feedback Loop Implementar cada uma das partes é mais fácil, pois o "contexto" é menor. Não precisamos subir a aplicação inteira para simular os cenários. Não é incomum conseguirmos implementar algo sem praticamente utilizar testes manuais (nada de alt+tab, fazer setup maluco, submit, ver stacktrace na cara e depois descobrir que fez um erro bobo de sintaxe no SQL.

E por fim, fazendo isso você tem direito ao selo "Engenharia By The Books" que te permite citar grandes nomes da indústria gratuitamente e postar imagens que te fazem parecer muito mais inteligente do que você realmente é.

Invoque a pirâmide de testes em uma conversa no café e talvez você ganhará um grupo no Orkut.

Mas veja bem, a literatura realmente parece certa. Não meça esforços ao criar muitos testes unitários para o core da sua aplicação, faça o que for preciso pra que eles sejam rápidos por que você terá vários deles (e se não tiver, isso é um problema). Normalmente você consegue isso apenas invertendo suas dependências e trocando as implementações em tempo de teste. Se você usará mocks, fakes ou uma implementação em memória alternativa siga o seu coração, o importante é feedback rápido e velocidade escalável. Tempere a gosto.

Adicione então uma penca de testes para garantir que a integração com o mundo externo está funcionando. Afinal se você usou dublês de testes nos testes unitários anteriores alguém precisa garantir que os verdadeiros fazem o seu trabalho. Não só por isso, mas também por quê você não irá testar todos os 8 casos de testes do seu busca_de_produtos_ativos_em_reposicao no usecase que utiliza esse método, já que não seria escalável testar isso tudo de novo toda vez que ele fosse reutilizado.

E por último os testes integrados (aka. end-to-end). Faça apenas o suficiente para garantir que seu sistema está minimamente OK. Eles são especialmente úteis em linguagens interpretadas, o que facilita muito na hora de fazer refatorações de estrutura ao começar a renomear e mover arquivos. Mas não se esqueça: eles são lentos! Use a dica de apenas um teste integrado por ação distinta do sistema, por exemplo, um para o GET e outro para o seu POST.

E é isso! Se ficou alguma dúvida, não deixe de perguntar. Se achou isso tudo um absurdo, comente aqui que talvez nós tenhamos ido longe de mais. Se curtiu tudo isso e quer trabalhar conosco ou achou uma bos**, mas quer nos ajudar a sair dessa, mande seu curriculum, pois estamos sempre em busca do chosen one.

Não tente isso em casa, a menos é claro que você resida no térreo.

--

--