Introdução ao Awaitlity

Introdução ao Awaitlity

*1. Introdução *

Um problema comum com sistemas assíncronos é que é difícil escrever testes legíveis para eles focados na lógica de negócios e não poluídos com sincronizações, tempos limite e controle de concorrência.

Neste artigo, veremos* Awaitility - uma biblioteca que fornece uma simples linguagem específica de domínio (DSL) para teste de sistemas assíncronos *.

Com awaitility, podemos expressar nossas expectativas do sistema em uma DSL fácil de ler.

*2. Dependências *

Precisamos adicionar dependências de Awaitility ao nosso pom.xml.

A biblioteca awaitility será suficiente para a maioria dos casos de uso. Caso desejemos usar as condições baseadas em proxy , , também precisamos fornecer a biblioteca awaitility-proxy:

<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility</artifactId>
    <version>3.0.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility-proxy</artifactId>
    <version>3.0.0</version>
    <scope>test</scope>
</dependency>

Você pode encontrar a versão mais recente do awaitility e _https://search.maven.org/classic/#search%7Cgav%7C1%7Cg%3A%22org.awaitility%22%20AND%20a%3A%22awaitility-proxy%22 [proxy de aguardabilidade] _ bibliotecas no Maven Central .

===* 3. Criando um serviço assíncrono *

Vamos escrever um serviço assíncrono simples e testá-lo:

public class AsyncService {
    private final int DELAY = 1000;
    private final int INIT_DELAY = 2000;

    private AtomicLong value = new AtomicLong(0);
    private Executor executor = Executors.newFixedThreadPool(4);
    private volatile boolean initialized = false;

    void initialize() {
        executor.execute(() -> {
            sleep(INIT_DELAY);
            initialized = true;
        });
    }

    boolean isInitialized() {
        return initialized;
    }

    void addValue(long val) {
        throwIfNotInitialized();
        executor.execute(() -> {
            sleep(DELAY);
            value.addAndGet(val);
        });
    }

    public long getValue() {
        throwIfNotInitialized();
        return value.longValue();
    }

    private void sleep(int delay) {
        try {
            Thread.sleep(delay);
        } catch (InterruptedException e) {
        }
    }

    private void throwIfNotInitialized() {
        if (!initialized) {
            throw new IllegalStateException("Service is not initialized");
        }
    }
}

===* 4. Testando comwaitility *

Agora, vamos criar a classe de teste:

public class AsyncServiceLongRunningManualTest {
    private AsyncService asyncService;

    @Before
    public void setUp() {
        asyncService = new AsyncService();
    }

   //...
}

Nosso teste verifica se a inicialização do nosso serviço ocorre dentro de um período de tempo limite especificado (padrão 10s) após a chamada do método initialize.

Esse caso de teste apenas espera que o estado de inicialização do serviço seja alterado ou lança uma ConditionTimeoutException se a alteração de estado não ocorrer.

O status é obtido por uma Callable que consulta nosso serviço em intervalos definidos (padrão de 100 ms) após um atraso inicial especificado (padrão de 100 ms). Aqui estamos usando as configurações padrão para tempo limite, intervalo e atraso:

asyncService.initialize();
await()
  .until(asyncService::isInitialized);

Aqui, usamos await - um dos métodos estáticos da classe Awaitility. Retorna uma instância de uma classe ConditionFactory. Também podemos usar outros métodos, como given, para aumentar a legibilidade.

Os parâmetros de tempo padrão podem ser alterados usando métodos estáticos da classe Awaitility:

Awaitility.setDefaultPollInterval(10, TimeUnit.MILLISECONDS);
Awaitility.setDefaultPollDelay(Duration.ZERO);
Awaitility.setDefaultTimeout(Duration.ONE_MINUTE);

Aqui podemos ver o uso da classe Duration, que fornece constantes úteis para os períodos de tempo mais usados.

Também podemos* fornecer valores de tempo personalizados para cada chamada await *. Aqui esperamos que a inicialização ocorra no máximo após cinco segundos e pelo menos após 100 ms com intervalos de pesquisa de 100 ms:

asyncService.initialize();
await()
    .atLeast(Duration.ONE_HUNDRED_MILLISECONDS)
    .atMost(Duration.FIVE_SECONDS)
  .with()
    .pollInterval(Duration.ONE_HUNDRED_MILLISECONDS)
    .until(asyncService::isInitialized);

Vale ressaltar que o ConditionFactory contém métodos adicionais como com, depois, e, dados. Esses métodos não fazem nada e apenas retornam isso, mas podem ser úteis para melhorar a legibilidade das condições de teste.

*5. Usando Matchers *

Awaitility também permite o uso de hamcrest matchers para verificar o resultado de uma expressão. Por exemplo, podemos verificar se nosso valor long é alterado conforme o esperado após chamar o método addValue:

asyncService.initialize();
await()
  .until(asyncService::isInitialized);
long value = 5;
asyncService.addValue(value);
await()
  .until(asyncService::getValue, equalTo(value));

Observe que neste exemplo, usamos a primeira chamada await para aguardar até que o serviço seja inicializado. Caso contrário, o método getValue lançaria uma IllegalStateException.

===* 6. Ignorando exceções *

Às vezes, temos uma situação em que um método lança uma exceção antes que um trabalho assíncrono seja concluído. Em nosso serviço, pode ser uma chamada para o método getValue antes de o serviço ser inicializado.

Awaitility fornece a possibilidade de ignorar essa exceção sem falhar em um teste.

Por exemplo, vamos verificar se o resultado getValue é igual a zero logo após a inicialização, ignorando IllegalStateException:

asyncService.initialize();
given().ignoreException(IllegalStateException.class)
  .await().atMost(Duration.FIVE_SECONDS)
  .atLeast(Duration.FIVE_HUNDRED_MILLISECONDS)
  .until(asyncService::getValue, equalTo(0L));

===* 7. Usando proxy *

Conforme descrito na seção 2, precisamos incluir awaitility-proxy para usar condições baseadas em proxy. A idéia do proxy é fornecer chamadas de métodos reais para condições sem implementação de uma expressão Callable ou lambda.

Vamos usar o método estático AwaitilityClassProxy.to para verificar se AsyncService foi inicializado:

asyncService.initialize();
await()
  .untilCall(to(asyncService).isInitialized(), equalTo(true));

===* 8. Acessando Campos *

Awaitility pode até acessar campos privados para executar afirmações neles. No exemplo a seguir, podemos ver outra maneira de obter o status de inicialização do nosso serviço:

asyncService.initialize();
await()
  .until(fieldIn(asyncService)
  .ofType(boolean.class)
  .andWithName("initialized"), equalTo(true));

===* 9. Conclusão*

Neste tutorial rápido, apresentamos a biblioteca Awaitility, familiarizamos-nos com sua DSL básica para o teste de sistemas assíncronos e vimos alguns recursos avançados que tornam a biblioteca flexível e fácil de usar em projetos reais.

Como sempre, todos os exemplos de código estão disponíveis no Github.