Devo injetar um Thread.sleeper como uma dependência para facilitar o teste da unidade?

5

Eu tenho um padrão repetido no meu código que não é fácil de testar: classes que são executadas periodicamente. Apenas para simplificar as coisas, digamos, assumir algo como:

while( running ){

    long millisToWait = scheduler.getMillisecondsUntilNextExecution();
    Thread.sleep(millisToWait);
    executeMyTask();
}

Onde Thread.sleep é um método estático que é difícil de alterar. Não importa se injetar um relógio personalizado no agendador porque alterar o relógio não alterará o tempo de inatividade. Eu acho que a solução pode ser criar uma classe ThreadSleeper.

System.currentMillis        -> replace by injected Clock.millis();
Thread.sleep(millisToWait)  -> replace by injected ThreadSleeper.sleep(millisToWait)

Em que ThreadSleeper pode ser algo como:

import java.util.concurrent.Semaphore;

public abstract class ThreadSleeper {

    public abstract void sleep(long millisecondsToSleep) throws InterruptedException;

    public static SystemThreadSleeper systemThreadSleeper(){
        return new SystemThreadSleeper();
    }

    public static ThreadSleeper testThreadSleeper(){
        return new TestThreadSleeper();
    }

    public static final class TestThreadSleeper extends ThreadSleeper{

        private Semaphore semaphore = new Semaphore(0);
        private Long millisecondsToSleep = null;

        //FIXME: make usable my multithread.
        //TODO: add method simulateMillisecondsPasses.

        @Override
        public synchronized void sleep(long millisecondsToSleep) throws InterruptedException {
            this.millisecondsToSleep = millisecondsToSleep;
            semaphore.acquire();
        }

        public synchronized long simulateSleepEndsReturnTimeSlept(){
            if(semaphore.availablePermits() < 0){
                semaphore.release();
                long slept = millisecondsToSleep;
                millisecondsToSleep = null;
                return slept;
            }else{
                return 0L;
            }
        }
    }

    public static final class SystemThreadSleeper extends ThreadSleeper{

        public void sleep(long millisecondsToSleep) throws InterruptedException {
            Thread.sleep(millisecondsToSleep);
        }
    }
}

Esta é uma prática boa / reanível? Eu me pergunto por que nunca vi algo assim. Pode ser que seja um exagero ou talvez haja melhores alternativas.

    
por Borjab 24.07.2018 / 17:49
fonte

4 respostas

4

Is this a reasonable/good practice?

Absolutamente.

Justificativa: o tempo é uma contribuição.

If you don't consider time an input value, think about it until you do - it is an important concept

John Carmack, 1998

É uma coisa normal em nossos testes isolados , para fornecer estratégias para o gerenciamento de efeitos colaterais. A passagem do tempo é um efeito colateral onde - em um teste - podemos querer uma estratégia alternativa.

Eu normalmente chegaria primeiro para uma interface, em vez de uma classe abstrata

interface Sleeper {
    void sleep(long durationInMillis) throws InterruptedException;
}

Você está absolutamente certo de que esta estratégia tem uma leitura correspondente que vai junto com ela - uma das coisas que esperamos quando dormimos é que depois as medições do relógio mudaram. As implementações dessas estratégias estão implicitamente acopladas - mas o chamador não precisa se importar.

Mais geralmente: no seu código de produção, você está tomando uma decisão de que o sono deve ser implementado usando encadeamentos java. Parnas nos ensina que esconder os detalhes dessas decisões por trás de um limite de módulo é uma boa ideia.

    
por 24.07.2018 / 21:58
fonte
3

Eu vou dizer isso de novo e de novo:

Relying on timing causes slow and fragile tests

Se você precisa garantir que uma tarefa foi concluída, então você precisa de algum outro mecanismo de sincronização do que simplesmente confiar no fato de que o tempo já passou. Se você estiver usando o C #, poderá simplesmente await a tarefa ser concluída. No entanto, existem outras opções como Barreiras que permitem que uma tarefa em segundo plano sinalize sua conclusão para a tarefa em primeiro plano.

Você usaria a barreira como esse pseudocódigo:

// Size is number of participants, one for the test and one for the task.
var barrier = new Barrier(2);

executeMyTask(barrier);
barrier.Await(); // can use variant with max ms to wait

Então, na sua tarefa, a última declaração seria:

barrier.Await();

Como o teste já estaria bloqueado, isso essencialmente desbloqueará o teste e essa tarefa.

E se o meu teste for sobre o tempo de duração da tarefa?

Você essencialmente tem um cronômetro (C # fornece uma classe para essa finalidade, ou você pode tirar timestamps antes e depois). Realize seu teste assim:

  • Iniciar o cronômetro
  • Iniciar a tarefa
  • Aguarde sua tarefa ou bloqueie com a Barreira
  • Pare o cronômetro
  • Afirme que o tempo gasto está dentro de uma margem de erro aceitável.

Na maioria das vezes, você só precisa se certificar de que a tarefa tenha menos de um determinado período de tempo, mas, se isso levar mais alguns milissegundos, talvez não seja um problema. Às vezes, você precisa garantir que o número de milissegundos tenha sido X, mas ainda há uma margem de erro aceitável.

    
por 24.07.2018 / 21:08
fonte
1

Parece que o método executeMyTask é a coisa que você deseja testar.

O outro bit é apenas um loop while simples que não precisa ser testado em unidade. Em vez de injetar um "sleeper thread" eu iria injetar um "executor de tarefas" ou "tarefa" que é executado:

while( running ){
    long millisToWait = scheduler.getMillisecondsUntilNextExecution();
    Thread.sleep(millisToWait);

    // Injected via constructor injection
    task.execute();
}

Agora você precisa de uma interface e de uma implementação concreta:

public interface ScheduledTask {
    void execute();
}

public class MyScheduledTask {
    public void execute() {
        // The thing you want to do
    }
}

Em seguida, você pode testar a unidade da classe MyScheduledTask , pois o método execute é público.

    
por 24.07.2018 / 18:57
fonte
0

Não há uma única resposta verdadeira, mas você pode unir o modo de dormir e obter o tempo atual em uma única interface. Porque ambos estão relacionados ao tempo, não são? Então, por zombaria, você poderia usar um valor incremental simples.

    
por 24.07.2018 / 18:59
fonte