Devo iniettare un Thread.sleeper come dipendenza per facilitare il test dell'unità?

5

Ho un pattern ripetuto nel mio codice che non è facile da testare: classi che vengono eseguite periodicamente. Solo per semplificare le cose, diciamo, assumiamo qualcosa del tipo:

while( running ){

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

Dove Thread.sleep è un metodo statico che è difficile da modificare. Non importa se iniettare un orologio personalizzato nello scheduler perché la modifica dell'orologio non modificherà il tempo di sospensione. Penso che la soluzione potrebbe essere quella di creare una classe ThreadSleeper.

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

Dove ThreadSleeper può essere qualcosa di simile;

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);
        }
    }
}

È una soluzione / buona pratica? Mi chiedo perché non abbia mai visto qualcosa di simile. Può essere un eccesso o forse ci sono alternative migliori.

    
posta Borjab 24.07.2018 - 17:49
fonte

4 risposte

4

Is this a reasonable/good practice?

Assolutamente.

Motivazione: il tempo è un input.

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

John Carmack, 1998

È una cosa normale nei nostri test isolati , fornire strategie per la gestione degli effetti collaterali. Il passare del tempo è un effetto collaterale in cui, in una prova, potremmo desiderare una strategia alternativa.

Normalmente raggiungo prima un'interfaccia, piuttosto che una classe astratta

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

Hai assolutamente ragione che questa strategia ha una lettura corrispondente che la segue - una delle cose che ci aspettiamo quando dormiamo è che in seguito le misure dell'orologio sono cambiate. Le implementazioni di queste strategie sono implicitamente accoppiate, ma il chiamante non ha bisogno di preoccuparsi.

Più in generale: nel tuo codice di produzione, stai facendo una decisione che il sonno dovrebbe essere implementato usando i thread java. Parnas ci insegna che nascondere i dettagli di queste decisioni dietro un limite del modulo è una buona idea.

    
risposta data 24.07.2018 - 21:58
fonte
3

Lo dirò più e più volte:

Relying on timing causes slow and fragile tests

Se è necessario assicurarsi che un'attività sia stata completata, è necessario un altro meccanismo di sincronizzazione piuttosto che basarsi sul fatto che è trascorso abbastanza tempo. Se stai utilizzando C #, puoi semplicemente await di completamento dell'attività. Tuttavia, ci sono altre opzioni come thread Barriere che consente a un'attività in background di segnalare il suo completamento all'attività in primo piano.

Userai la barriera qualcosa come questo pseudocodice:

// 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

Quindi nel tuo compito, l'ultima affermazione sarebbe:

barrier.Await();

Poiché il test sarebbe già stato bloccato, in pratica questo sbloccherà il test e questa attività.

Che cosa succede se il mio test riguarda il tempo impiegato dall'attività?

In pratica hai un cronometro (C # fornisce una classe per questo scopo, oppure puoi prendere timestamp prima e dopo). Esegui il test in questo modo:

  • Avvia il cronometro
  • Avvia l'attività
  • Attendi il tuo compito o blocca con la Barriera
  • Interrompi il cronometro
  • Asserisci che il tempo impiegato è all'interno di un margine di errore accettabile.

La maggior parte delle volte devi solo assicurarti che l'attività sia inferiore a un certo periodo di tempo, ma se impiega qualche millisecondo in più potrebbe non essere un problema. A volte devi assicurarti che occorra effettivamente un numero X di millisecondi, ma c'è ancora un margine di errore accettabile.

    
risposta data 24.07.2018 - 21:08
fonte
1

Sembra che il metodo executeMyTask sia la cosa che vuoi testare.

L'altro bit è solo un semplice ciclo while che non ha bisogno di essere testato sull'unità. Invece di iniettare un "thread sleeper" vorrei iniettare un "task executor" o "task" che viene eseguito:

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

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

Ora hai bisogno di un'interfaccia e di un'implementazione concreta:

public interface ScheduledTask {
    void execute();
}

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

Quindi puoi testare unitamente la classe MyScheduledTask poiché il suo metodo execute è pubblico.

    
risposta data 24.07.2018 - 18:57
fonte
0

Non esiste un'unica risposta vera, ma puoi unire il sonno e ottenere il tempo corrente in un'unica interfaccia. Perché sono entrambi legati al tempo, no? Quindi per il mocking potresti usare un semplice valore incrementale.

    
risposta data 24.07.2018 - 18:59
fonte