Kotno: Testiranje asinhronskih stvari v ponarejenem območju ASync VS. zagotavljanje načrtovalcev po meri

Večkrat so me spraševali o "lažni coni" in kako jo uporabljati. Zato sem se odločil, da bom napisal ta članek, da bom delil svoja opažanja, ko gre za finozrnate teste „fakeAsync“.

Območje je ključni del kotnega ekosistema. Lahko bi kdo prebral, da je cona samo nekakšen „kontekst izvedbe“. Pravzaprav kotne opice pošiljajo globalne funkcije, kot sta setTimeout ali setInterval, da bi prestregle funkcije, ki se izvajajo po nekaj zamudi (setTimeout) ali občasno (setInterval).

Pomembno je omeniti, da ta članek ne bo pokazal, kako ravnati s kraji za setTimeout. Ker Angular močno uporablja RxJ-je, kar se naslanja na izvorne funkcije za merjenje časa (morda ste presenečeni, vendar je res), uporablja cono kot zapleteno, a močno orodje za beleženje vseh asinhronih dejanj, ki lahko vplivajo na stanje aplikacije. Kotno jih prestreže, da bi vedeli, ali je v čakalni vrsti še nekaj dela. Izprazni čakalno vrsto, odvisno od vremena. Najverjetneje izpraznjene naloge spremenijo vrednosti spremenljivk komponent. Kot rezultat, se predloga ponovno upodobi.

Zdaj vse stvari o asinhciji niso tisto, za kar moramo skrbeti. Prav lepo je razumeti, kaj se dogaja pod pokrovom, ker pomaga pisati učinkovite teste. Poleg tega razvojni test ima velik vpliv na izvorno kodo („TDD-ovi izvori so bili želja po močnem samodejnem regresijskem testiranju, ki podpira evolucijsko zasnovo. Na poti so njeni praktiki odkrili, da so pisni testi najprej znatno izboljšali proces oblikovanja. "Martin Fowler, https://martinfowler.com/articles/mocksArentStubs.html, 09/2017).

Kot rezultat vseh teh prizadevanj lahko premaknemo čas, ki ga potrebujemo za preizkus stanja v določenem času.

fakeAsync / označi oris

Kotni dokumenti navajajo, da fakeAsync (https://angular.io/guide/testing#fake-async) ponuja bolj linearno izkušnjo kodiranja, ker se znebi obljub, kot je .whenStable (). Nato (…).

Koda znotraj bloka fakeAsync izgleda tako:

klop (100); // počakajte, da se prva naloga opravi
fixture.detectChanges (); // posodobiti pogled s ponudbo
odkljukati (); // počakajte, da se druga naloga opravi
fixture.detectChanges (); // posodobiti pogled s ponudbo

Naslednji delčki ponujajo nekaj vpogleda v način delovanja fakeAsync.

tukaj se uporabljajo setTimeout / setInterval, ker jasno prikazujejo, kdaj se funkcije izvršijo v območju fakeAsync. Lahko pričakujete, da mora ta funkcija „it“ vedeti, kdaj je test opravljen (v Jasmine razporejen po argumentu: Funkcija), vendar se tokrat zanašamo na spremljevalca fakeAsync, ne pa na uporabo kakršnega koli povratnega klica:

it ('izprazni consko nalogo po nalogi', fakeAsync (() => {
        setTimeout (() => {
            pustimo i = 0;
            const ročaj = setInterval (() => {
                če (i ++ === 5) {
                    clearInterval (ročaj);
                }
            }, 1000);
        }, 10000);
}));

Glasno se pritožuje, ker je v čakalni vrsti še nekaj »timerjev« (= setTimeouts):

Napaka: 1 čas (-i) je še vedno v čakalni vrsti.

Očitno je, da moramo premakniti čas za izvedbo časovno izbrane funkcije. Parametrizirani „kljukico“ dodamo z 10 sekundami:

klopec (10000);

Hugh? Napaka postane bolj zmedena. Zdaj test ne uspe zaradi vklenjenih „periodičnih časovnikov“ (= setIntervals):

Napaka: 1 redni časovniki so še vedno v čakalni vrsti.

Ker smo se vpisali v funkcijo, ki jo je treba izvajati vsako sekundo, moramo čas znova uporabiti tudi s klopom. Funkcija se konča po 5 sekundah. Zato moramo dodati še 5 sekund:

klopec (15000);

Zdaj je test mimo. Vredno je povedati, da območje prepozna naloge, ki se izvajajo vzporedno. Preprosto podaljšajte časovno izbrano funkcijo z drugim klicem setInterval.

it ('izprazni consko nalogo po nalogi', fakeAsync (() => {
    setTimeout (() => {
        pustimo i = 0;
        const ročaj = setInterval (() => {
            če (++ i === 5) {
                clearInterval (ročaj);
            }
        }, 1000);
        naj bo j = 0;
        const handle2 = setInterval (() => {
            če (++ j === 3) {
                clearInterval (ročaj2);
            }
        }, 1000);
    }, 10000);
    klopec (15000);
}));

Preizkus še vedno poteka, ker sta se oba omenjena spletna obdobja začela v istem trenutku. Obe sta opravljeni, ko mine 15 sekund:

fakeAsync / odkljukajte v akciji

Zdaj vemo, kako delujejo fakeAsync / krpelj. Naj uporablja za nekatere smiselne stvari.

Razvijmo polje, podobno predlogu, ki izpolnjuje te zahteve:

  • rezultat pridobi iz nekaterih API-jev (storitev)
  • ugasne uporabniški vnos, da počaka na končni iskalni izraz (zmanjša število zahtevkov); DEBOUNCING_VALUE = 300
  • prikaže rezultat v uporabniškem vmesniku in odda ustrezno sporočilo
  • preizkus enote spoštuje asinhrono naravo kode in preizkuša pravilno obnašanje polja, podobnega predlogu, glede na pretečeni čas

Zaključimo s temi scenariji testiranja:

opis ('pri iskanju', () => {
    it ('počisti prejšnji rezultat', fakeAsync (() => {
    }));
    it ('oddaja začetni signal', fakeAsync (() => {
    }));
    it ("ustavlja možne zadetke API-ja na 1 zahtevo na DEBOUNCING_VALUE milisekund", fakeAsync (() => {
    }));
});
description ('ob uspehu', () => {
    it ("kliče google API", fakeAsync (() => {
    }));
    it ('odda signal o uspehu s številom tekem', fakeAsync (() => {
    }));
    it ('prikazuje naslove v predlagalnem polju', fakeAsync (() => {
    }));
});
description ('napak', () => {
    it ('odda signal napake', fakeAsync (() => {
    }));
});

Pri „on search“ ne čakamo na rezultat iskanja. Ko uporabnik poda vnos (na primer „Lon“), je treba počistiti prejšnje možnosti. Pričakujemo, da bodo možnosti prazne. Poleg tega je treba uporabnikov vnos izklopiti, recimo z vrednostjo 300 milisekund. Glede na cono je v čakalno vrsto potisnjeno 300 milijonov mikrotačk.

Upoštevajte, da za podrobnost izpuščam nekaj podrobnosti:

  • nastavitev testa je približno enaka kot pri kotnih dokumentih
  • primerek apiService se vbrizga prek fixture.debugElement.injector (…)
  • SpecUtils sproži uporabniške dogodke, kot sta vnos in fokus
predEach (() => {
    spyOn (apiService, 'poizvedba') in.returnValue (opazljivo.of (queryResult));
});
fit ('počisti prejšnji rezultat', fakeAsync (() => {
    comp.options = ['ne prazno'];
    SpecUtils.focusAndInput ('Lon', napeljava, 'vhod');
    kljukica (DEBOUNCING_VALUE);
    fixture.detectChanges ();
    pričakovati (comp.options.length) .toBe (0, `was [$ {comp.options.join (',')}]`);
}));

Koda komponente, ki poskuša zadovoljiti test:

ngOnInit () {
    this.control.valueChanges.debounceTime (300) .odjavi (vrednost => {
        this.options = [];
        this.suggest (vrednost);
    });
}
predlagaj (q: niz) {
    this.googleBooksAPI.query (q) .prijavite se (rezultat => {
// ...
    }, () => {
// ...
    });
}

Pojdimo skozi kodo korak za korakom:

Vohunimo po poizvedbeni metodi apiService, ki jo bomo poklicali v komponenti. Spremenljivka queryResult vsebuje nekaj posmehljivih podatkov, kot so „Hamlet“, „Macbeth“ in „King Lear“. Na začetku pričakujemo, da bodo možnosti prazne, a kot ste morda opazili, se celotno čakalno vrsto fakeAsync izprazni s klopom (DEBOUNCING_VALUE), zato komponenta vsebuje tudi končni rezultat Shakespearovih spisov:

Pričakovano, da bo 3 0, "je bil [Hamlet, Macbeth, King Lear]".

Za zahtevo poizvedbe o storitvi potrebujemo zakasnitev, da posnemamo asinhroni čas, ki ga porabi klic API. Dodajmo 5 sekund zamude (REQUEST_DELAY = 5000) in odkljukajte (5000).

predEach (() => {
    spyOn (apiService, 'poizvedba'). in.returnValue (opazljivo.of (queryResult) .delay (1000));
});

fit ('počisti prejšnji rezultat', fakeAsync (() => {
    comp.options = ['ne prazno'];
    SpecUtils.focusAndInput ('Lon', napeljava, 'vhod');
    kljukica (DEBOUNCING_VALUE);
    fixture.detectChanges ();
    pričakovati (comp.options.length) .toBe (0, `was [$ {comp.options.join (',')}]`);
    kljukica (REQUEST_DELAY);
}));

Po mojem mnenju bi ta primer moral delovati, vendar Zone.js trdi, da je v čakalni vrsti še nekaj dela:

Napaka: 1 redni časovniki so še vedno v čakalni vrsti.

Na tem mestu moramo iti globlje, da vidimo tiste funkcije, za katere sumimo, da so se v coni zataknile. Določitev nekaterih točk preloma je pot:

odpravljanje napak v območju ponarejenega zaslona

Nato to izdajte v ukazni vrstici

_fakeAsyncTestZoneSpec._scheduler._schedulerQueue [0] .args [0] [0]

ali preglejte vsebino območja, kot je ta:

hmmm, metoda splakovanja AsyncScheduler je še vedno na vrsti ... zakaj?

Ime funkcije, ki se jo aktivira, je metoda splakovanja AsyncScheduler.

javni flush (akcija: AsyncAction ): void {
  const {Actions} = to;
  če (ta.aktiven) {
    action.push (akcija);
    vrnitev;
  }
  naj napaka: kakršna koli;
  this.active = res;
  narediti {
    če (napaka = action.execute (action.state, action.delay)) {
      odmor;
    }
  } while (action = dejanja.shift ()); // izčrpajte čakalno vrsto planerja
  this.active = napačno;
  če (napaka) {
    medtem, ko (action = Actions.shift ()) {
      action.unsubscribe ();
    }
    napaka pri metanju;
  }
}

Zdaj se boste morda vprašali, kaj je narobe z izvorno kodo ali cono.

Težava je v tem, da območje in naši klopi niso sinhronizirani.

Sam cona ima trenutni čas (2017), medtem ko želi klopa obdelati dejanje, predvideno 01.01.1970 + 300 milis + 5 sekund.

Vrednost planerja asinhronizacije potrjuje, da:

uvoz {async kot AsyncScheduler} iz 'rxjs / planer / async';
// to postavite nekje znotraj „it“
console.info (AsyncScheduler.now ());
// → 1503235213879

AsyncZoneTimeInSyncKeeper v reševanje

Eden od možnih popravkov za to je imeti pripomoček za stalno sinhronizacijo, kot je ta:

izvozni razred AsyncZoneTimeInSyncKeeper {
    čas = 0;
    konstruktor () {
        spyOn (AsyncScheduler, 'zdaj'). in.callFake (() => {
            / * tslint: onemogoči naslednjo vrstico * /
            console.info ('čas', ta čas);
            vrni to.time;
        });
    }
    kljukica (čas ?: številka) {
        if (typeof time! == 'undefined') {
            this.time + = čas;
            kljukica (to.time);
        } else {
            odkljukati ();
        }
    }
}

Spremlja trenutni čas, ki ga vrne zdaj (), kadarkoli se prikliče planer async. To deluje, ker funkcija tick () uporablja isti trenutni čas. Oba, načrtovalca in cona, si delita isti čas.

Priporočam, da v fazi beforeEach sprostite časInSyncKeeper:

opis ('pri iskanju', () => {
    pustite časInSyncKeeper;
    predEach (() => {
        timeInSyncKeeper = nov AsyncZoneTimeInSyncKeeper ();
    });
});

Zdaj pa si poglejmo uporabo varovalca za časovno sinhronizacijo. Upoštevajte, da se moramo spoprijeti s to časovno težavo, ker je besedilno polje razveljavljeno in zahteva traja nekaj časa.

opis ('pri iskanju', () => {
    pustite časInSyncKeeper;
    predEach (() => {
        timeInSyncKeeper = nov AsyncZoneTimeInSyncKeeper ();
        spyOn (apiService, 'poizvedba'). in.returnValue (Opazljiv.of (queryResult) .delay (REQUEST_DELAY));
    });
    it ('počisti prejšnji rezultat', fakeAsync (() => {
        comp.options = ['ne prazno'];
        SpecUtils.focusAndInput ('Lon', napeljava, 'vhod');
        timeInSyncKeeper.tick (DEBOUNCING_VALUE);
        fixture.detectChanges ();
        pričakovati (comp.options.length) .toBe (0, `was [$ {comp.options.join (',')}]`);
        timeInSyncKeeper.tick (REQUEST_DELAY);
    }));
    // ...
});

Pojdimo skozi ta primer po vrstico:

  1. instanca primerka skrbnika sinhronizacije
timeInSyncKeeper = nov AsyncZoneTimeInSyncKeeper ();

2. po odhodu REQUEST_DELAY odgovorite na metodo apiService.query z rezultatom queryResult. Recimo, da je poizvedovalna metoda počasna in se odzove po REQUEST_DELAY = 5000 milisekundah.

spyOn (apiService, 'poizvedba'). in.returnValue (opazljivo.of (queryResult) .delay (REQUEST_DELAY));

3. Pretvarjajte se, da je v polju za predloge prisotna možnost „prazno“

comp.options = ['ne prazno'];

4. Pojdite na polje „input“ v osnovnem elementu vgradnje in vstavite vrednost „Lon“. To simulira interakcijo uporabnika z vnosnim poljem.

SpecUtils.focusAndInput ('Lon', napeljava, 'vhod');

5. prepustite časovno obdobje DEBOUNCING_VALUE v ponarejenem območju asinhronizacije (DEBOUNCING_VALUE = 300 milisekund).

timeInSyncKeeper.tick (DEBOUNCING_VALUE);

6. Zaznajte spremembe in ponovno upodobite predlogo HTML.

fixture.detectChanges ();

7. Niz možnosti je zdaj prazen!

pričakovati (comp.options.length) .toBe (0, `was [$ {comp.options.join (',')}]`);

To pomeni, da je bilo mogoče opazne vrednosti Spremembe, uporabljene v komponentah, izvajati ob pravem času. Upoštevajte, da je izvedena funkcija debounceTime-d

vrednost => {
    this.options = [];
    this.onEvent.emit ({signal: SuggestSignal.start});
    this.suggest (vrednost);
}

potisnil drugo nalogo v čakalno vrsto s klicanjem predlagane metode:

predlagaj (q: niz) {
    če (! q) {
        vrnitev;
    }
    this.googleBooksAPI.query (q) .prijavite se (rezultat => {
        če (rezultat) {
            this.options = result.items.map (item => item.volumeInfo);
            this.onEvent.emit ({signal: SuggestSignal.success, totalItems: result.totalItems});
        } else {
            this.onEvent.emit ({signal: SuggestSignal.success, totalItems: 0});
        }
    }, () => {
        this.onEvent.emit ({signal: SuggestSignal.error});
    });
}

Spomnite se na vohuna po metodi poizvedb API-ja google books, ki se odzove po 5 sekundah.

8. Nazadnje moramo znova odkljukati za REQUEST_DELAY = 5000 milisekund, da izpustimo čakalno vrsto. Za opazovanje, na katerega se naročimo v predlagani metodi, je treba izpolniti REQUEST_DELAY = 5000.

timeInSyncKeeper.tick (REQUEST_DELAY);

fakeAsync…? Zakaj? Obstajajo planerji!

Strokovnjaki ReactiveX lahko trdijo, da bi lahko uporabili testne načrtovalce, da bi opazili preizkusili. Za kotne aplikacije je mogoče, vendar ima nekaj pomanjkljivosti:

  • od vas se morate seznaniti z notranjo strukturo opazovalcev, operaterji,…
  • kaj, če imate v svoji prijavi nekaj grdih setTimeout? Planerji jih ne obravnavajo.
  • najpomembnejši: Prepričan sem, da ne želite uporabljati načrtovalcev v celotni aplikaciji. Ne želite mešati proizvodne kode s testi enote. Ne želite narediti kaj takega:
const testScheduler;
če (okolje.test) {
    testScheduler = nov YourTestScheduler ();
}
pustiti opazno;
če (testScheduler) {
    opazljiv = Observable.of ('value'). zamuda (1000, testScheduler)
} else {
    opazljiv = opazljiv.of ('vrednost'). zamuda (1000);
}

To ni izvedljiva rešitev. Po mojem mnenju je edina možna rešitev "vbrizgati" načrtovalca testa z zagotavljanjem vrste "pooblaščencev" za prave Rxjs metode. Upoštevati je treba še, da lahko prevladujoče metode negativno vplivajo na preostale enotne teste. Zato bomo uporabljali Jasmineove vohune. Vohuni se očistijo po vsakem.

Funkcija monkeypatchScheduler zavije prvotno implementacijo Rxjs s pomočjo vohuna. Vohun sprejme argumente metode in po potrebi priloži testScheduler.

import {IScheduler} iz 'rxjs / Scheduler';
import {Observable} iz 'rxjs / Observable';
prijavi var spyOn: Funkcija;
izvozna funkcija monkeypatchScheduler (planer: IScheduler) {
    pustite observableMethods = ['concat', 'defer', 'empty', 'forkJoin', 'if', 'interval', 'spoji', 'of', 'range', 'met',
        'zadrga]];
    pustite operatorMethods = ['pufer', 'concat', 'delay', 'razločno', 'do', 'every', 'last', 'spoji', 'max', 'take',
        'timeInterval', 'lift', 'debounceTime'];
    naj injectFn = funkcija (osnova: poljubno, metode: string []) {
        method.forEach (metoda => {
            const orig = osnova [metoda];
            če (typeof orig === 'funkcija') {
                spyOn (osnova, metoda). in.callFake (funkcija () {
                    naj args = Array.prototype.slice.call (argumenti);
                    if (args [args.length - 1] && typeof args [args.length - 1] .now === 'funkcija') {
                        args [args.length - 1] = planer;
                    } else {
                        args.push (planer);
                    }
                    return orig.apply (to, argumenti);
                });
            }
        });
    };
    injectFn (Opazljivi, opazljiviMetode);
    injectFn (Observable.prototype, operatorMethods);
}

Od zdaj naprej bo testScheduler opravil vsa dela znotraj Rxjs. Ne uporablja setTimeout / setInterval ali kakršnih koli stvari async. Ponarejenega zaznavanja ni več potrebe.

Zdaj potrebujemo primerek preskusnega planerja, ki ga želimo prenesti na monkeypatchScheduler.

Obnaša se podobno kot privzeti TestScheduler, vendar zagotavlja način povratnega klica onAction. Tako vemo, katera akcija je bila izvedena po tem obdobju.

izvozni razred SpyingTestScheduler podaljša VirtualTimeScheduler {
    spyFn: (actionName: niz, zamuda: številka, napaka ?: kakršen koli) => void;
    konstruktor () {
        super (VirtualAction, defaultMaxFrame);
    }
    onAction (spyFn: (actionName: niz, zamuda: številka, napaka ?: kakršen koli) => void) {
        this.spyFn = spyFn;
    }
    flush () {
        const {dejanja, maxFrames} = to;
        pustite napako: katero koli, dejanje: AsyncAction ;
        medtem ko ((action = Actions.shift ()) && (this.frame = action.delay) <= maxFrames) {
            pustite stateName = this.detectStateName (dejanje);
            pusti zamudo = action.delay;
            če (napaka = action.execute (action.state, action.delay)) {
                če (this.spyFn) {
                    this.spyFn (stanjeName, zamuda, napaka);
                }
                odmor;
            } else {
                če (this.spyFn) {
                    this.spyFn (ime države, zamuda);
                }
            }
        }
        če (napaka) {
            medtem, ko (action = Actions.shift ()) {
                action.unsubscribe ();
            }
            napaka pri metanju;
        }
    }
    zasebno detectStateName (dejanje: AsyncAction ): string {
        const c = Object.getPrototypeOf (action.state) .konstruktor;
        const argsPos = c.toString (). indexOf ('(');
        če (argsPos! == -1) {
            vrne c.toString (). podvrsto (9, argsPos);
        }
        vrniti ničelno;
    }
}

Za konec si oglejmo uporabo. Primer je isti preizkus enote, kot je bil uporabljen prej (zbriše prejšnji rezultat) z majhno razliko, da bomo namesto fakeAsync / odkljukali uporabo testnega razporejevalnika.

naj testScheduler;
predEach (() => {
    testScheduler = nov SpyingTestScheduler ();
    testScheduler.maxFrames = 1000000;
    monkeypatchScheduler (testScheduler);
    fixture.detectChanges ();
});
predEach (() => {
    spyOn (apiService, 'poizvedba'). in.callFake (() => {
        vrniti Observable.of (queryResult) .delay (REQUEST_DELAY);
    });
});
it ('počisti prejšnji rezultat', (opravljeno: funkcija) => {
    comp.options = ['ne prazno'];
    testScheduler.onAction ((actionName: niz, zamuda: število, napaka ?: kakršen koli) => {
        če (actionName === 'DebounceTimeSubscriber' && delay === DEBOUNCING_VALUE) {
            pričakovati (comp.options.length) .toBe (0, `was [$ {comp.options.join (',')}]`);
            Končano();
        }
    });
    SpecUtils.focusAndInput ("Londo", napeljava, "vhod");
    fixture.detectChanges ();
    testScheduler.flush ();
});

Ustvarjalec preskusnega urnika je prvi in ​​pred vsakim ustvaril (!). V drugem predEach vohunimo po apiService.query, da bi postregli z rezultatom queryResult po REQUEST_DELAY = 5000 milisekund.

Zdaj pa pojdimo skozi to po vrstici:

  1. Najprej upoštevajte, da razglasimo za opravljeno funkcijo, ki jo potrebujemo v povezavi s klicem povratnega klica preizkuševalca. To pomeni, da moramo Jasmine povedati, da test opravimo sami.
it ('počisti prejšnji rezultat', (opravljeno: funkcija) => {

2. Spet se pretvarjamo, da so nekatere komponente prisotne v komponenti.

comp.options = ['ne prazno'];

3. To zahteva nekaj razlage, ker se zdi na prvi pogled nekoliko neroden. Počakati želimo na akcijo, imenovano „DebounceTimeSubscriber“, z zamudo DEBOUNCING_VALUE = 300 milisekund. Ko se to zgodi, želimo preveriti, ali je opcija.length 0. Nato je test končan in pokličemo done ().

testScheduler.onAction ((actionName: niz, zamuda: število, napaka ?: kakršen koli) => {
    if (actionName === 'DebounceTimeSubscriber' && delay === DEBOUNCING_VALUE) {
      pričakovati (comp.options.length) .toBe (0, `was [$ {comp.options.join (',')}]`);
      Končano();
    }
});

Vidite, da uporaba preskusnih načrtovalcev zahteva nekaj posebnega znanja o notranjosti izvajanja Rxjs. Seveda je odvisno, kateri preizkuševalski program uporabljate, toda tudi če sami izvajate močan načrtovalnik, boste morali razumeti načrtovalce in izpostaviti nekatere vrednosti izvajanja za prilagodljivost (kar pa spet ne more biti samoumevno).

4. Ponovno uporabnik vnese vrednost „Londo“.

SpecUtils.focusAndInput ("Londo", napeljava, "vhod");

5. Spet zaznajte spremembe in ponovno upodabljajte predlogo.

fixture.detectChanges ();

6. Končno izvedemo vsa dejanja, ki so postavljena v čakalni vrsti planerja.

testScheduler.flush ();

Povzetek

Kotni lastni pripomočki za testiranje so rajši kot tisti, ki jih izdelujejo sami, dokler delujejo. V nekaterih primerih par fakeAsync / klopov ne deluje, vendar ni nobenega razloga, da bi obupali in izpustili enotne teste. V teh primerih je pot do orodja za samodejno sinhronizacijo (tukaj znanega tudi kot AsyncZoneTimeInSyncKeeper) ali prilagojenega načrtovalca preizkušanja (tukaj poznamo tudi kot SpyingTestScheduler).

Izvorna koda