WHYYY Y NO PASS??

Um problema chato foi apontado na retrospectiva final de um projeto recente em que a Lambda3 entregou (por falar nisso: com sucesso, dentro do prazo, do escopo, do custo, com testes e código limpo e deixando o cliente feliz). Nossos testes de integração end-to-end deram muito trabalho. Muitas vezes inconsistentes, eles nos tomaram um tempo de desenvolvimento maior do que gostaríamos.

Explicando um pouquinho da arquitetura: era um projeto web, ASP.NET MVC utilizando para armazenamento Azure Tables, Queues e Blobs. O projeto tem demanda de escala, respondendo a milhões de usuário (demanda de milhares concorrentes simultâneos), por isso foi hospedado em Azure websites, com autoscaling. Há também um worker role processando algumas filas que vem do Azure Queues. Chegamos a responder dez mil requests por minuto com tempo de resposta para o usuário de 1 segundo e meio enquanto ficando abaixo de 30% de processamento nas dez máquinas médias do front-end, muito acima do desempenho esperado pelo cliente, e ainda com muito espaço pra crescer. Lógico que isso tudo foi feito com async e await, viva o C#5! Mas isso é assunto pra outro post.

Além dos testes de unidade de C# e CoffeeScript fizemos também testes via web browser utilizando Selenium WebDriver. É muito comum utilizarmos WebDriver na Lambda3, utilizamos em projetos C#, Java, Node, etc. Ele tem suas idiossincrasias, mas no geral é o melhor que encontramos até agora. Ultimamente temos rodado ele usando o PhantomJS, que é um headless browser, e permite rodar tudo bem mais rápido do que seria se usássemos um browser com interface gráfica, como o Firefox, Chrome ou IE. Sempre mantemos a opção de trocar um teste ou outro para Chrome, por exemplo, para debugar um possível problema. No último projeto tivemos 100% dos testes end-to-end utilizando o Phantom, foi muito bom.

No entanto, um testes desse tipo tem muitos possíveis pontos de falha. Desde o emulador do Azure Storage, passando pelo IIS Express, até o WebDriver junto com o Phantom, muita coisa pode falhar. Um erro na hora de subir o emulador do Azure, uma falha de conexão no driver do Phantom, uma demora inesperada do IIS Express para subir o site, e pronto: um teste falha. E temos centenas de testes, demora para rodar, de repente um teste falhar por um motivo inesperado é muito frustrante. Aí você imagina que é um erro, mas não é, foi um teste que nunca falha, falhando. É incontrolável. Rodar o teste novamente não adianta: ele passa.

Resolvemos muitos desses problemas, muitos são determinísticos, falham dentro de um percentual esperado. Aos poucos fomos tornando a suite de testes mais resiliente. Por exemplo: esperamos requests ajax terminarem, antes de clicar em um item verificarmos se há um handler anexado (útil para o caso de elementos que recebem dinamicamente um event handler via JavaScript), esperamos a página carregar por completo, e por aí vai. Ainda assim, há o lado caótico da coisa, imprevisível. Idealmente conseguiríamos resolver tudo, mas depois de pegar tantos casos, ficou evidente que não é possível antecipar todo e qualquer problema sem gastar um custo proibitivo para o projeto. E tudo ficou pior quando começamos a usar SPAs, onde há ainda mais volatilidade no browser. Erros de elementos não encontrados, ou que foram removidos da página, por exemplo, pipocam aos montes.

Fiquei responsável por resolver esse problema para o próximo projeto. E agora acho que tenho um caminho razoável que vamos usar na Lambda3, e achamos interessante compartilhar com vocês o resultado dessa busca. Resolvemos que seria razoável rodar um teste desses que falha de maneira caótica mais algumas vezes para confirmar o erro antes de falhar tudo. A ideia é tentar executar o teste de novo, e se passar seguimos em frente. Fui avaliar opções, precisaria de suporte no executor do teste, algum tipo de plugin. Não queríamos ter que alterar todo o código do projeto. A ideia é alterar o framework ou o executor de teste, não a suite de testes.

Fui primeiro avaliar o NUnit. O modelo de plugins é ruim, depende de instalações na máquina e não funcionou como eu esperava. Além disso, não há um plugin para o que eu queria fazer, e construir um ia dar um trabalho bem grande. A instalação no servidor de build também não iria funcionar, a maneira que o TFS (que faz nosso build) roda, com test adapters, não suporta ainda plugins do NUnit.

Voltei ao xUnit. Não olhava ele fazia um bom tempo, o padrão era NUnit e ele funciona bem, então fiquei longe dele esses últimos anos. Foi o criador do NUnit que o desenvolveu, e segundo ele porque aprendeu com os erros e queria fazer algo muito melhor. E confirmei: é muito melhor. O modelo de plugins é muito bom, os padrões são excelentes, e o suporte ao Visual Studio é bem completo. Também não havia um plugin pronto para o que eu queria, mas montar um parecia trivia. Fui lá e fiz. E aproveitei e fiz também um plugin para o SpecFlow, que é quem roda a maior parte dos nossos tests end-to-end.

O resultado foram dois pacotes Nuget:

O código fonte dos dois projetos está no Github.

Pra usar é bem simples, há exemplo no Github, mas basicamente você coloca um atributo “Retry” no método de teste:

[Retry(5)] //will try 5 times
public void TryAFewTimes()
{
    tried++;
    Assert.True(tried >= 5);
}

Você não precisa colocar o número de tentativas, o padrão é 3:

[Retry] //note there is no count here
public void TryAFewTimes()
{
    tried++;
    Assert.True(tried >= 3);
}

No Specflow basta usar uma tag. Pode ser no cenário:

@retry(8)
Feature: A feature with retries

Com ou sem o número, assim como no código C# direto:

@retry
Feature: A feature with default retries

Você também pode usar direto no cenário, com ou sem o número de tentativas. Se houver a tag no cenário e na feature, a da feature toma precedência:

@retry(12)
Scenario: Try something
    When I try something 10 times

@retry
Scenario: Try something
    When I try something 3 times

Você pode ler os testes todos no Github.

É bom lembrar: esse componente não deve ser usado para todos os seus testes, somente para esse caso específico. Testes inconsistentes são um bad smell, então evite ao máximo. O ideal é não ter. Mas, se você tiver, esse componente pode te ajudar. Fica de presente pra comunidade, e a licença é GPL2, ou seja, fiquem a vontade para contribuir e evoluir o código como acharem melhor.

E aí, o que acharam? Vale a pena usar? Vocês tem problemas parecidos?