Testes Unitários em Web Crawlers

Guilherme José Acra
Sigalei
Published in
6 min readSep 28, 2020
Imagem por Марьян Блан em Unsplash

Qualquer programador profissional reconhece a importância de escrever testes para um código que roda ou irá rodar em produção. O primeiro tipo de teste que costuma-se implementar em um sistema é o Teste Unitário, por ser “o de mais baixo nível”.

Aqui na Sigalei, desde que lemos o livro Clean Code de Robert C. Martin, entendemos que um código bem feito, que irá resultar em um produto de qualidade, necessita de testes unitários. Daí pra frente, tudo que ouvíamos e líamos ao longo de nossas carreiras apenas fortalecia essa ideia. Não poderíamos mais desenvolver códigos não testados!

Importância dos testes unitários

Testes unitários são códigos “extras” em seu projeto, que testam o código “real” (aquele que vai para produção e será usado pelos clientes). Os testes unitários chamam porções bem pequenas do seu código (métodos, funções, classes, …), passam argumentos e checam o retorno. Se eu tenho uma função que soma dois números, meu teste irá chamá-la passando (2, 2) e checar se retorna 4, depois chamá-la com (-1, 6) e ver se retorna 5 e assim vai, com vários casos de teste.

São diversos os benefícios de usar teste unitários. Eles:

  • Ajudam a encontrar bugs o mais cedo possível;
  • Documentam o código;
  • Ajudam a projetar seu sistema (caso sejam feitos anteriormente);
  • Dão mais tranquilidade de entregar o projeto sabendo que os testes passaram;
  • Tiram o medo de mudar alguma coisa em um código já estabelecido e quebrar algo.

Testes unitários não são importantes para web crawlers?

Nós do time responsável pelas extrações de dados aqui na Sigalei fomos procurar, então, como implementar testes unitários em web crawlers. Qual foi a nossa surpresa quando descobrimos que praticamente ninguém estava falando sobre isso. E mais, muitos estavam dizendo que não era importante fazer testes unitários para web crawlers (ou que era demasiadamente difícil), e que o foco deveria ser em monitorar a extração ao longo do tempo.

Dado tudo que tínhamos estudado sobre testes unitários, sobre como eles eram fundamentais na construção de um código de qualidade, seja qual for o objetivo desse código, ficamos pensando:

Seriam os web crawlers softwares tão diferentes dos demais a ponto da necessidade de testes unitários não ser verdade para esse único caso específico?

Capa satírica de um livro técnico em que se lê: Your application is a special snowflake: Excuses for not writing Unit Tests

Não queríamos acreditar nisso. Web Crawlers são softwares como qualquer outro e, portanto, era importante que escrevêssemos testes para eles. Se a questão era a dificuldade de implementação, precisaríamos pensar em uma arquitetura adequada.

Monitoramento vs testes unitários

Vale ressaltar que nunca negamos a importância do monitoramento dos web crawlers. Os sites dos quais extraímos nossos dados podem sair do ar ou mudar o layout a qualquer momento, e desde muito cedo analisamos logs e métricas para gerar alertas de quando algo está errado.

Entretanto isso não tira o mérito dos testes unitários. Estes garantem a qualidade em outro domínio. Por mais que o monitoramento garanta que estamos extraindo os dados, ele, no geral, não garante que os dados estão realmente corretos e estruturados de acordo com um modelo pré-definido. Para o nosso caso de extração de projetos de lei, ficam as perguntas: Será que estamos pegando o nome do projeto corretamente? E o status dele? Será que eu fazendo essa correção para esse projeto de lei específico eu estarei causando problemas em outros tipos de projetos que quem fez o código original sabia que existia, mas eu não? Apenas testes unitários podem ajudar a resolver essas dúvidas.

Por quê implementar testes unitários em web crawler é tão difícil?

Ao tentar implementar nosso primeiro teste unitário em um de nossos web crawlers, descobrimos porquê a tarefa não é tão fácil assim. Aqui na Sigalei, nós usamos um framework chamado Scrapy e, nas poucas discussões online sobre testes unitários para web crawlers, vimos que ele possui uma feature chamada Contracts que parecia ser perfeita para o que queríamos.

Usando-a poderíamos passar uma URL e avaliar se nosso código extraia o número de itens corretos e os atributos desses itens. O Scrapy faz uma request para a URL passada, chama o código de parsing do nosso crawler e compara a saída real com a que declaramos no Contract. Parecia perfeito, se não fosse por um pequeno problema. O conteúdo da URL que passamos pode mudar. Um projeto de lei que estava ativo no dia que fizemos o teste, pode ser aprovado logo no dia seguinte, e o teste irá falhar. Não pois o código está errado (o código nem mudou!), mas porque o teste passou a estar errado, uma vez que o item declarado como certo no Contract não corresponde mais ao do site.

Além disso, já estávamos usando o Pytest para testar outras partes do nosso código, como conectores com as bases de dados, e queríamos unificar todos os testes em um mesmo formato. O Pytest facilita demais a implementação de vários casos de teste, algo que queríamos muito, uma vez que cada projeto de lei possui suas especificidades (uns possuem documentos e outros não, por exemplo), e precisávamos testar a extração de cada um deles.

Como implementamos testes unitários em crawlers na Sigalei

Para resolver o problema do conteúdo das páginas poder mudar, sabíamos que teríamos que baixar e armazenar esse conteúdo. Quando fossemos testar o código, usaríamos a resposta baixada anteriormente e não a resposta real. Dessa maneira, mesmo que as informações do site mudem, isso não quebraria o teste, uma vez que estamos usando uma versão da resposta da época em que fizemos as saídas esperadas.

Inicialmente pensamos em salvar as páginas em arquivos no próprio projeto, mas consideramos que isso iria poluí-lo e deixar a revisão de código um tanto confusa. Decidimos, então, armazenar as páginas de teste em um repositório remoto de arquivos (Cloud Storage do GCP ou S3 da AWS, por exemplo).

A arquitetura da solução se encontra na figura abaixo:

Diagrama da arquitetura adotada. O diagrama será explicado no texto a seguir.

Fizemos um script em Python para rapidamente baixar a resposta de uma URL de teste (1) e subir no repositório remoto com um nome definido (2). No projeto de um novo crawler, coletamos todas as URLs de teste necessárias e salvamos elas usando este script.

Na implementação do teste, declaramos vários casos de teste, cada um com o nome da resposta salva no repositório remoto e com o item extraído esperado (escrito manualmente) (3).

O teste em si, consiste em (para cada caso de teste):

  • Baixar a resposta armazenada no repositório remoto (4);
  • Montar um objeto Response do Scrapy “falso” com o conteúdo da resposta baixada;
  • Passar essa Response para uma função de parse de um crawler (chamado de Spider, no Scrapy) (5);
  • Comparar o item retornado com o esperado (6).
  • Se a saída do crawler for igual a esperada o teste passa, caso contrário falha (7).

Um exemplo de implementação seria (é importante conhecer o Scrapy e o Pytest para entender completamente):

Mas e se o site mudar?

Como já falado, não é incomum que o formato do site que estamos extraindo mude, o que nos obriga a adequar o código de parsing do crawler afetado. Quando isso ocorre, temos que atualizar todas as respostas salvas no repositório remoto para a versão mais recente do site.

Isso, infelizmente, acaba tomando um tempo considerável, dependendo do número de casos de teste. Entretanto, consideramos isso como algo natural: uma mudança no site extraído pode ser considerada como uma mudança nos requisitos do sistema, e, se os requisitos mudaram, é justo que os testes também tenham que mudar. Além disso, é uma dificuldade aceitável frente às vantagens de se possuir testes unitários.

--

--