Testar threads é difícil, muito difícil, e isso faz com que escrever bons testes de integração para sistemas multithread seja… difícil. Isso porque no JUnit não há recurso nativo de sincronização entre os códigos de teste, objetos sob teste e todas as threads. Isso significa que os problemas normalmente surgem quando você tem que escrever um teste para um método que cria e executa uma thread. Um dos cenários mais comuns nessa área é fazer uma chamada para um método sob teste, o qual inicia uma nova thread executada antes do retorno. Em algum ponto no futuro, quando a tarefa da thread tiver terminado e você precisará verificar se tudo ocorreu bem. Exemplos de um cenário como esse incluem a leitura assíncrona de dados de um socket ou um longo e complexo conjunto de operações em uma base de dados.
Por exemplo, a classe ThreadWrapper abaixo contém um único método público: doWork(). Executar doWork() inicia a operação e, em algum ponto no futuro, com a discrição da JVM, uma thread é executada para acrescentar dados à base.
[java]
public class ThreadWrapper {
/**
* Start the thread running so that it does some work.
*/
public void doWork() {
Thread thread = new Thread() {
/**
* Run method adding data to a fictitious database
*/
@Override
public void run() {
System.out.println("Start of the thread");
addDataToDB();
System.out.println("End of the thread method");
}
private void addDataToDB() {
// Dummy Code…
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread.start();
System.out.println("Off and running…");
}
}
[/java]
Um teste objetivo para esse código seria chamar o método doWork() e então checar a base de dados pelo resultado. O problema é que, pertencendo ao uso da thread, não há uma ordenação entre um objeto sob teste, o teste e a thread. Uma forma comum de atingir alguma ordenação coletiva ao escrever esse tipo de teste é colocar algum atraso entre a chamada para um método e a checagem dos resultados na bases de dados, conforme demonstrado abaixo:
[java]
public class ThreadWrapperTest {
@Test
public void testDoWork() throws InterruptedException {
ThreadWrapper instance = new ThreadWrapper();
instance.doWork();
Thread.sleep(10000);
boolean result = getResultFromDatabase();
assertTrue(result);
}
/**
* Dummy database method – just return true
*/
private boolean getResultFromDatabase() {
return true;
}
}
[/java]
No código acima, há um simples Thread.sleep(10000) entre as duas chamadas de métodos. Essa técnica tem o benefício de ser incrivelmente simples; entretanto, é também bastante arriscada. Isso porque introduz uma condição de concorrência (race condition) entre o teste e a thread de trabalho, uma vez que a JVM não garante quando as threads serão executadas. Acontece com frequência de isso funcionar na máquina do desenvolvedor para falhar consistentemente na máquina de produção. Mesmo que o exemplo funcione na máquina de produção, ela aumenta artificialmente a duração do teste; e lembre-se de que um build rápido é importante.
A única maneira segura de fazer isso de forma correta é sincronizar as duas diferentes threads. E uma técnica para fazer isso é incluir um simples CountDownLatch dentro da instância sob teste. No exemplo abaixo eu modifiquei o método doWork() da classe ThreadWrapper ao acrescentar o CountDownLatch como um argumento.
[java]
public class ThreadWrapper {
/**
* Start the thread running so that it does some work.
*/
public void doWork(final CountDownLatch latch) {
Thread thread = new Thread() {
/**
* Run method adding data to a fictitious database
*/
@Override
public void run() {
System.out.println("Start of the thread");
addDataToDB();
System.out.println("End of the thread method");
countDown();
}
private void addDataToDB() {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void countDown() {
if (isNotNull(latch)) {
latch.countDown();
}
}
private boolean isNotNull(Object obj) {
return latch != null;
}
};
thread.start();
System.out.println("Off and running…");
}
}
[/java]
A documentação da API do Java descreve um CountDownLatch como:
Um auxílio à sincronização que permite que uma ou mais threads esperem até que um conjunto de operações que estão sendo realizadas em outras threads sejam completadas. Um CountDownLatch é inicializado com uma dada contagem. O método await bloqueia a thread até que a contagem atual alcance zero devido a invocações do método countDown(), depois do qual todas as threads são liberadas, e quaisquer invocações subsequentes ao await retornarão imediatamente. Esse é um fenômeno único, e a contagem não pode ser reiniciada. Se precisar reiniciar a contagem, considere utilizar um CyclicBarrier.
Um CountDownLatch é uma ferramenta de sincronização versátil e que pode ser utilizada para inúmeros propósitos. Um CountDownLatch inicializado com uma simples contagem de um serve como um gate: todas as threads invocando await esperam pela passagem até que ela seja aberta pela thread que invoca o countDown(). Um CountDownLatch inicializado com N pode ser usado para fazer uma thread esperar até N para completar alguma ação, ou que alguma ação tenha sido completada N vezes.
Uma propriedade útil da função CountDownLatch é que ela não necessita que as threads chamando pelo countDown esperem até que a contagem chegue em zero antes de prosseguir, o que simplesmente previne qualquer thread de prosseguir depois de um await até que todas as threads tenham passado.
A ideia aqui é que código de teste nunca checará a base de dados pelos resultados até que o método run() da thread de trabalho tenha chamado latch.countdown(). Isso porque a thread do código de teste está bloqueando até a chamar pelo latch.await(). A latch.countdown() decresce a contagem do latch e, uma vez que ela atinge zero, a chamada latch.await()de bloqueio envia o retorno e o código de teste continua a execução, seguro de que qualquer resultado que deva estar na base de dados realmente estará na base de dados. O teste pode retornar os resultados e realizar uma checagem válida.
Obviamente, o código acima apenas finge as operações e as conexões na base de dados…
A questão é que você pode não querer, ou não precisar, incluir um CountDownLatch diretamente no seu código; afinal, ele não é usado em produção, e essa solução não parece particularmente limpa ou elegante. Uma forma rápida de resolver isso é simplesmente tornar o método doWork(CountDownLatch latch) privado e expô-lo ele através de um método doWork() público.
[java]
public class ThreadWrapper {
/**
* Start the thread running so that it does some work.
*/
public void doWork() {
doWork(null);
}
@VisibleForTesting
void doWork(final CountDownLatch latch) {
Thread thread = new Thread() {
/**
* Run method adding data to a fictitious database
*/
@Override
public void run() {
System.out.println("Start of the thread");
addDataToDB();
System.out.println("End of the thread method");
countDown();
}
private void addDataToDB() {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void countDown() {
if (isNotNull(latch)) {
latch.countDown();
}
}
private boolean isNotNull(Object obj) {
return latch != null;
}
};
thread.start();
System.out.println("Off and running…");
}
}
[/java]
O código acima utiliza a notação @VisibleForTesting do Google Gava para nos dizer que a visibilidade do método doWork(CountDownLatch latch) foi diminuída para fins de testes.
Noto agora que tornar privado o método de chamada a um pacote para fins de teste é algo altamente controverso; algumas pessoas odeiam a ideia, enquanto que outras incluem-na em todos os lugares. Eu poderia escrever um artigo sobre esse assunto (e talvez o faça um dia), mas para mim é algo a ser usado de maneira prudente, quando não há outra opção. Por exemplo, quando estiver escrevendo testes de caracterização para código legado. Se possível, deve ser evitado, mas nunca descartado. Afinal, código testado é melhor do que código não testado.
Com isso em mente, a próxima interação com o ThreadWrapper projeta a necessidade de um método marcado como @VisibleForTesting juntamente com a necessidade de injetar um CountDownLatch no código em produção. A ideia aqui é usar o modelo estratégico (Strategy Pattern) e separar a implementação Runnable da Thread. Por isso, temos um ThreadWrapper bastante simples.
[java]
public class ThreadWrapper {
/**
* Start the thread running so that it does some work.
*/
public void doWork(Runnable job) {
Thread thread = new Thread(job);
thread.start();
System.out.println("Off and running…");
}
}
[/java]
e um job separado
[java]
public class DatabaseJob implements Runnable {
/**
* Run method adding data to a fictitious database
*/
@Override
public void run() {
System.out.println("Start of the thread");
addDataToDB();
System.out.println("End of the thread method");
}
private void addDataToDB() {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
[/java]
Você notará que a classe DatabaseJob não usa um CountDownLatch. Como ela é sincronizada, então? A resposta está no código de teste abaixo…
[java]
public class ThreadWrapperTest {
@Test
public void testDoWork() throws InterruptedException {
ThreadWrapper instance = new ThreadWrapper();
CountDownLatch latch = new CountDownLatch(1);
DatabaseJobTester tester = new DatabaseJobTester(latch);
instance.doWork(tester);
latch.await();
boolean result = getResultFromDatabase();
assertTrue(result);
}
/**
* Dummy database method – just return true
*/
private boolean getResultFromDatabase() {
return true;
}
private class DatabaseJobTester extends DatabaseJob {
private final CountDownLatch latch;
public DatabaseJobTester(CountDownLatch latch) {
super();
this.latch = latch;
}
@Override
public void run() {
super.run();
latch.countDown();
}
}
}
[/java]
O código de teste acima contém a classe interna DatabaseJobTester, a qual estende DatabaseJob. Nessa classe, o método run() foi sobrescrito para incluir uma chamada ao latch.countDown() depois que a nossa base de dados falsa foi atualizada pelas chamadas feitas pela to super.run(). Isso funciona porque o teste passa a instância DatabaseJobTester para o método doWork(Runnable job) acrescentando a capacidade necessária para o teste de thread.
Para concluir:
- Testar threads é difícil.
- Testar classes internas anônimas é quase impossível.
- Utilizar Thead.sleep(...) é uma ideia arriscada e deve ser evitada.
- Você pode refatorar esses problemas utilizando o modelo estratégico.
- Programar é a arte de tomar a decisão certa
O código acima está disponível no Github no repositório captain debug (git://github.com/roghughe/captaindebug.git) dentro do projeto unit-testing-threads.
***
Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://www.captaindebug.com/2013/02/synchronising-multithreaded-integration.html#.UeVnUeFD7YV