• Nenhum resultado encontrado

mento, há determinadas propriedades que devem ser verdadeiras ao fim da operação e que podem ser verificadas automaticamente via asserções. Quando elas não são verdadeiras em algumas das execuções, têm-se as falhas esporádicas em testes, cuja causa é normalmente difícil de ser detectada.

No entanto, algumas falhas esporádicas não são devidas a defeitos no software, mas sim a problemas com os testes. Para melhor compreender tais problemas, a seguir serão introdu- zidos o processo de testes de operações assíncronas e as técnicas comumente utilizadas para se determinar o momento de se fazer asserções em tais testes.

2.2

Esperando antes de Asserções

Em geral, quando há operações assíncronas a se testar, as seguintes fases devem estar pre- sentes nos testes:

1. Exercitar o sistema;

2. Esperar; e

3. Fazer asserções.

Para ilustrar estas fases, pode-se considerar mais uma vez o exemplo do teste da iniciali- zação de um sistema composto por vários serviços para saber se tais serviços foram iniciados e ficaram prontos para receber requisições.

Na fase 1 de tal teste teria-se a seguinte operação: mySystem.initialize(). Essa operação inicia o sistema, e para isso, cria outras threads, que podem, por sua vez, criar outras, representando os diferentes serviços. Todas essas threads irão executar algumas ope- rações e então esperar por outros estímulos. Este exemplo está ilustrado pela Figura 2.1. Nesta figura mostra-se um caso em que a operação mySystem.initialize() levou à iniciação de uma thread B, que por sua vez iniciou duas threads do tipo A. Uma dessas threads A criou duas threads do tipo C.

Considerando esse mesmo exemplo, a fase de asserções (fase 3) deste teste pode apre- sentar o seguinte código:assertTrue(isCorrectlyInitialized(mySystem)). Esse código verifica se o sistema foi corretamente iniciado, podendo verificar, por exemplo,

2.2 Esperando antes de Asserções 16

Figura 2.1: Exemplo: Threads criadas por uma invocação feita por um teste

se o estado de seus serviços, representado por variáveis do sistema, está "ATIVO"e se suas filas de requisições estão vazias. Caso o sistema não tenha sido corretamente iniciado (por exemplo, estando em um estado diferente ou apresentando requisições em sua fila), o teste irá falhar.

Entretanto, como a inicialização é assíncrona, antes de verificar se o sistema foi correta- mente iniciado, deve-se esperar até que o processo de iniciação tenha sido concluído, uma fase da execução do sistema que pode ser mapeada em um certo estado para as suas threads (e.g. threads do tipo C finalizadas e threads dos tipos A e B em espera). Identificar quanto tempo esperar antes das asserções, especialmente quando múltiplas threads são criadas, pode ser desafiador e requerer muitas mudanças do código. Além disso, dependendo da aborda- gem utilizada para fazer o teste esperar, algumas falhas de testes podem acontecer, como será detalhado adiante.

De maneira geral, quando há operações assíncronas em um teste, algumas das formas comuns de implementar a fase de espera (fase 2) antes das asserções são:

1. Atrasos explícitos. Invocações a operações que fazem a thread do teste esperar por um tempo fixo, passado como parâmetro. Ex:

Thread.sleep(t);

2. Espera ocupada. Laços que apresentam atrasos explícitos em seu interior e que verifi- cam de tempos em tempos uma determinada propriedade a respeito do sistema até que a condição verificada no laço seja verdadeira. Ex:

while (!stateReached()) Thread.sleep(t);

2.2 Esperando antes de Asserções 17

3. Controle das threads. Barreiras de sincronização ou operações que fazem a thread do teste esperar enquanto outras threads atingem um estado determinado, como por exemplo o estado “terminadas” (finished). Ex:

myThread.join();

Atrasos explícitos e espera ocupada

Atrasos explícitos (explicit delays) podem ser facilmente encontrados em testes, especial- mente por serem a abordagem de mais fácil implementação. No entanto, seu uso pode resul- tar nos seguintes problemas: testes que não passam devido ao uso de intervalos insuficientes de espera; ou testes que podem levar muito tempo para executar devido ao uso de intervalos de espera muito longos [66] (p. 255). Essa técnica assume que após um certo intervalo de tempo t o sistema estará em um estado específico, o que nem sempre é verdade dependendo do ambiente de execução dos testes. Um outro problema que o uso desta técnica pode gerar são asserções tardias, caso a espera seja grande demais e o sistema não esteja mais no estado esperado. O problema principal desta técnica é que o tempo adequado para esperar depende da máquina sendo utilizada e da carga a que está sendo submetida, o que faz com que depen- dendo do intervalo de tempo a esperar adotado, um teste falhe em uma máquina e passe em outra.

O uso da técnica de espera ocupada (busy wait) também é comum, especialmente quando a condição sendo verificada a cada iteração de seu laço é facilmente obtida do sistema. É fácil encontrá-la em códigos Open Source e discussões na Internet [76], por exemplo. Porém, o uso de espera ocupada apresenta algumas desvantagens [60] (p. 196-197): i) tal técnica pode levar a um desperdício de tempo de CPU devido a várias execuções do laço sem necessidade (o que nem sempre é crítico, dependendo do tipo de teste); ii) pode-se passar do ponto certo em que a condição verificada é verdadeira por alguns instantes (algo que pode acontecer também com atrasos explícitos); iii) podem-se usar determinadas construções de linguagens de programação que podem não ser efetivas no sentido de deixar outras threads executarem (como oThread.yield()de Java, que depende das políticas da JVM sendo usada).

É importante destacar também que quando a condição de espera utilizada é a mesma que se pretende verificar nas asserções, é indicado que se use uma versão mais elaborada de

2.2 Esperando antes de Asserções 18

espera ocupada baseada em estouro de tempo (timeouts). Isso deve ser feito para evitar que a execução do teste entre em um laço infinito caso a aplicação apresente um defeito que a impeça de atingir aquele estado. Essa situação também pode ser desejável mesmo quando a condição de espera não é a mesma da asserção. Um exemplo de código com espera ocupada baseada em estouro de tempo é mostrado a seguir:

begin = Now();

while (! condicao() && (Now() - begin) < TEST_TIMEOUT) { Thread.sleep(t);

}

Controle das Threads

Uma maneira mais segura de saber quando uma asserção pode ser executada é através do controle das threads criadas pelo teste. Deve-se garantir que elas terminaram ou que estão em um estado estável (ex: todas já iniciaram e alcançaram um estado de espera enquanto requisições não chegam) antes que as asserções aconteçam. Quando se tem no teste as instâncias dos objetos que representam as threads, isso é mais simples, e operações como

join()da classeThreadde Java podem ser invocadas. Tal operação faz com que o teste só prossiga quando a thread tiver concluído o seu trabalho.

No entanto, nem sempre é possível ter acesso a todas as instâncias de todas as threads criadas por um teste. Algumas vezes, uma monitoração adicional das threads da aplicação é necessária para indicar quando fazer as asserções. Além disso, precisa-se, por vezes, de algum mecanismo de controle, como barreiras de sincronização, para que se garanta que a asserção seja feita de forma segura, sem que se passe do ponto, evitando assim que algumas

threads acordem no momento em que asserções estão sendo executadas e possam interferir

nos resultados de teste, fazendo com que embora o estado esperado tenha sido alcançado, o sistema já tenha saído desse estado no momento em que a asserção é executada. Por exemplo, no teste da inicialização do sistema em que se verifica se seus serviços estão ativos e se as filas de requisição estão vazias, tais filas podem não mais estar nesse estado pelo fato de alguma thread ter acordado e gerado novas requisições.