Nurga all: asünkri kraami testimine fakedAsynci tsoonis VS. kohandatud ajakavade pakkumine

Minult on mitu korda küsitud küsimusi võltstsooni ja selle kasutamise kohta. Seetõttu otsustasin kirjutada selle artikli, et jagada oma tähelepanekuid peeneteraliste fakeAsynci testide kohta.

Tsoon on nurgaökosüsteemi oluline osa. Võib olla võis lugeda, et tsoon ise on lihtsalt omamoodi „täitmiskontekst“. Tegelikult suunab Nurgaline ahv selliseid globaalseid funktsioone nagu setTimeout või setInterval, et kinni pidada funktsioonidest, mida täidetakse pärast mõnda viivitust (setTimeout) või perioodiliselt (setInterval).

Oluline on mainida, et see artikkel ei näita, kuidas setTimeout häkkidega hakkama saada. Kuna Angular kasutab intensiivselt RxJ-sid, mis toetuvad loomulikele ajafunktsioonidele (võite olla üllatunud, kuid see on tõsi), kasutab ta tsooni kui keerulist, kuid võimsat tööriista kõigi asünkroonsete toimingute registreerimiseks, mis võivad rakenduse olekut mõjutada. Nurgeline võtab nad kinni, et teada saada, kas järjekorras on veel tööd. See tühjendab järjekorra sõltuvalt kellaajast. Tõenäoliselt muudavad tühjendatud ülesanded komponendi muutujate väärtusi. Selle tulemusel mall malli uuesti renderdatakse.

Nüüd pole kogu asünkri värk see, mille pärast peame muretsema. Kapoti all toimuvast on lihtsalt tore aru saada, kuna see aitab kirjutada tõhusaid ühikatseid. Lisaks mõjutab testipõhine arendamine lähtekoodile tohutut mõju („TDD lähtepunktiks oli soov saada tugev automaatne regressioonitesti, mis toetas evolutsioonilist disaini. Selle aja jooksul, mil selle praktikud avastasid, et testide kirjutamine parandas esmalt oluliselt disainiprotsessi. “Martin Fowler, https://martinfowler.com/articles/mocksArentStubs.html, 09/2017).

Kõigi nende jõupingutuste tulemusel saame aega nihutada, kuna peame seisukorra kontrollimiseks konkreetsel ajahetkel.

fakeAsync / linnuke kontuur

Nurgadokumentides öeldakse, et fakeAsync (https://angular.io/guide/testing#fake-async) pakub lineaarsemat kodeerimise kogemust, kuna see vabaneb lubadustest, näiteks .whenStable (), siis (…).

FakeAsynci ploki sees olev kood näeb välja järgmine:

linnuke (100); // oodake esimese ülesande täitmist
fixture.detectChanges (); // värskenda vaadet tsitaadiga
linnuke (); // oodake, kuni teine ​​ülesanne saab valmis
fixture.detectChanges (); // värskenda vaadet tsitaadiga

Järgmised lõigud annavad fakeAsynci toimimisviisist ülevaate.

setTimeout / setInterval kasutatakse siin, kuna need näitavad selgelt, millal funktsioonid fakeAsynci tsoonis käivitatakse. Võib arvata, et see „it“ funktsioon peab teadma, millal test on tehtud (Jasmises vastavalt argumendile tehtud funktsioon: Funktsioon), kuid seekord tugineme fakeAsynci kaaslasele, mitte mingisuguse tagasihelistamise kasutamisele:

it ('tühjendab tsooniülesande ülesande kaupa', fakeAsync (() => {
        setTimeout (() => {
            las i = 0;
            const käepide = setInterval (() => {
                if (i ++ === 5) {
                    clearInterval (käepide);
                }
            }, 1000);
        }, 10000);
}));

See kurdab valjusti, sest järjekorras on veel mõnda taimerit (= setTimeouts):

Viga: 1 taimer on endiselt järjekorras.

On ilmne, et aegunud funktsiooni täitmiseks peame aega nihutama. Lisame parameetrilise „linnukese“ 10 sekundiga:

linnuke (10000);

Hugh? Viga muutub segasemaks. Nüüd ebaõnnestub test sisestatud perioodiliste taimerite (= setIntervals) tõttu:

Viga: 1 perioodiline taimer on endiselt järjekorras.

Kuna me vallutasime funktsiooni, mida tuleb täita igal sekundil, peame ka aega nihutama, kasutades uuesti linnuke. Funktsioon lakkab 5 sekundi pärast. Seetõttu peame lisama veel 5 sekundit:

linnuke (15000);

Nüüd on test möödas. Tasub öelda, et tsoon tunneb ära paralleelselt töötavad ülesanded. Lihtsalt pikendage aegunud funktsiooni teise setIntervali kõne abil.

it ('tühjendab tsooniülesande ülesande kaupa', fakeAsync (() => {
    setTimeout (() => {
        las i = 0;
        const käepide = setInterval (() => {
            kui (++ i === 5) {
                clearInterval (käepide);
            }
        }, 1000);
        laske j = 0;
        const käepide2 = setInterval (() => {
            kui (++ j === 3) {
                clearInterval (käepide2);
            }
        }, 1000);
    }, 10000);
    linnuke (15000);
}));

Test on endiselt läbitav, kuna mõlemad setIntervallid on käivitatud samal hetkel. Mõlemad on tehtud 15 sekundi möödumisel:

fakeAsync / linnuke tegevuses

Nüüd me teame, kuidas fakeAsync / linnuke töötab. Laske seda kasutada mõtestatud asjade jaoks.

Arendame välja ettepanekutaolise välja, mis vastab järgmistele nõuetele:

  • see haarab mõne API (teenuse) tulemuse
  • see vispeldab kasutaja sisestatud andmeid, et oodata lõplikku otsingusõna (see vähendab taotluste arvu); DEBOUNCING_VALUE = 300
  • see näitab tulemust kasutajaliideses ja väljastab vastava teate
  • ühiku test arvestab koodi asünkroonse olemusega ja testib vihjeta välja õiget käitumist läbitud aja osas

Me lõpetame selle testimisstsenaariumiga:

kirjelda ('otsimisel', () => {
    see ('kustutab eelmise tulemuse', fakeAsync (() => {
    }));
    see ('väljastab stardisignaali', fakeAsync (() => {
    }));
    see ('visandab API võimalike kokkulangemiste arvu 1 taotlusele DEBOUNCING_VALUE millisekundi kohta', fakeAsync (() => {
    }));
});
kirjelda ('õnnestumisel', () => {
    see ('kutsub üles google API', fakeAsync (() => {
    }));
    it ('kiirgab edu signaali vastete arvuga', fakeAsync (() => {
    }));
    see ('näitab pealkirju soovitaval väljal', fakeAsync (() => {
    }));
});
kirjelda ('veal', () => {
    see ('väljastab tõrkesignaali', fakeAsync (() => {
    }));
});

Kui otsisite, ei oota me otsingutulemust. Kui kasutaja annab sisendi (näiteks „Lon“), tuleb eelmised valikud kustutada. Eeldame, et võimalused on tühjad. Lisaks tuleb kasutaja sisendit drosseerida, öeldes väärtuseks 300 millisekundit. Tsooni osas lükatakse järjekorda 300 milliliitrit mikrotaset.

Pange tähele, et jätan lühiduse huvides välja mõned üksikasjad:

  • testimisseaded on peaaegu samad, mida näha nurgadokumentide puhul
  • apiService'i eksemplar sisestatakse faili fixture.debugElement.injector kaudu (…)
  • SpecUtils käivitab kasutajaga seotud sündmused, näiteks sisendi ja fookuse
beforeEach (() => {
    spyOn (apiService, 'päring') ja.returnValue (Observable.of (queryResult));
});
sobib ('kustutab eelmise tulemuse', fakeAsync (() => {
    comp.options = ['mitte tühi'];
    SpecUtils.focusAndInput ('Lon', kinnitus, 'input');
    linnuke (DEBOUNCING_VALUE);
    fixture.detectChanges ();
    oodata (comp.options.length) .toBe (0, `oli [$ {comp.options.join (',')}] '));
}));

Komponendi kood, mis proovib testi täita:

ngOnInit () {
    this.control.valueChanges.debounceTime (300) .subscribe (value => {
        see.valikud = [];
        this.suggest (väärtus);
    });
}
soovita (q: string) {
    this.googleBooksAPI.query (q) .subscribe (tulemus => {
// ...
    }, () => {
// ...
    });
}

Vaatame koodi samm-sammult läbi:

Me nuhkime apiService päringumeetodit, mida me selles komponendis kutsume. Muutuja queryResult sisaldab mõningaid pilkavaid andmeid, näiteks „Hamlet“, „Macbeth“ ja „King Lear“. Alguses eeldame, et valikud on tühjad, kuid nagu võisite märgata, siis kogu fakeAsynci järjekord tühjendatakse linnukesega (DEBOUNCING_VALUE) ja seetõttu sisaldab komponent ka Shakespeare'i kirjutiste lõpptulemust:

Eeldatavasti 3 oleks 0, „oli [Hamlet, Macbeth, kuningas Lear]”.

Vajame teenuse päringutaotlusega viivitust, et jäljendada API-kõne jaoks kulutatud aja asünkroonset möödumist. Lisage 5 sekundi viivitus (REQUEST_DELAY = 5000) ja tehke linnuke (5000).

beforeEach (() => {
    spyOn (apiService, 'päring') ja.returnValue (Observable.of (queryResult) .delay (1000));
});

sobib ('kustutab eelmise tulemuse', fakeAsync (() => {
    comp.options = ['mitte tühi'];
    SpecUtils.focusAndInput ('Lon', kinnitus, 'input');
    linnuke (DEBOUNCING_VALUE);
    fixture.detectChanges ();
    oodata (comp.options.length) .toBe (0, `oli [$ {comp.options.join (',')}] '));
    linnuke (REQUEST_DELAY);
}));

Minu arvates peaks see näide toimima, kuid Zone.js väidab, et järjekorras on veel tööd:

Viga: 1 perioodiline taimer on endiselt järjekorras.

Siinkohal peame minema sügavamale, et näha neid funktsioone, mille kohta arvame, et nad jäävad tsooni kinni. Mõne vahepunkti määramine on tee:

silumine fakeAsynci tsoonis

Seejärel väljastage see käsurealt

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

või uurige tsooni sisu järgmiselt:

hmmm, AsyncScheduleri loputamismeetod on endiselt järjekorras ... miks?

Vallatud funktsiooni nimi on AsyncScheduleri loputamismeetod.

avalik masendus (tegevus: AsyncAction <ükskõik)> tühine {
  const {tegevused} = see;
  if (this.active) {
    toimingud.push (tegevus);
    tagasi;
  }
  lase viga: ükskõik;
  this.active = true;
  tee {
    if (viga = action.execute (action.state, action.delay)) {
      vaheaeg;
    }
  } while (tegevus = action.shift ()); // ammendage planeerija järjekord
  see.aktiivne = vale;
  if (viga) {
    while (tegevus = action.shift ()) {
      action.uns tellida ();
    }
    viske viga;
  }
}

Nüüd võite küsida, mis on viga lähtekoodi või tsooni endaga.

Probleem on selles, et tsoon ja meie puugid pole sünkroonis.

Tsoonil endal on praegune aeg (2017), samal ajal kui linnuke soovib töödelda 01.01.1970 + 300 millisi + 5 sekundit.

Asünkrograafi väärtus kinnitab, et:

impordi {async asyncScheduler} kaustast 'rxjs / planeerija / async';
// asetage see kuskile "it" sisse
console.info (AsyncScheduler.now ());
// → 1503235213879

AsyncZoneTimeInSyncKeeper appi

Üks võimalik lahendus selle jaoks on sünkroonimisutiliidi olemasolu:

ekspordiklass AsyncZoneTimeInSyncKeeper {
    aeg = 0;
    ehitaja () {
        spyOn (AsyncScheduler, 'nüüd') ja.callFake (() => {
            / * tslint: keela järgmine rida * /
            console.info ('aeg', seek.time);
            tagasta see aeg;
        });
    }
    linnuke (kellaaeg ?: arv) {
        if (typeof time! == 'määratlemata') {
            this.time + = aeg;
            linnuke (seekord);
        } veel {
            linnuke ();
        }
    }
}

See jälgib praegust aega, mille saab nüüd () tagasi, kui asünkroonitaja kutsutakse. See töötab, kuna funktsioon linnuke () kasutab sama kellaaega. Nii planeerija kui ka tsoon jagavad sama aega.

Soovitan kiirendada timeInSyncKeeper eelmises etapis:

kirjelda ('otsimisel', () => {
    lase timeInSyncKeeper;
    beforeEach (() => {
        timeInSyncKeeper = uus AsyncZoneTimeInSyncKeeper ();
    });
});

Vaatame nüüd aja sünkroonimise hoidja kasutamist. Pidage meeles, et peame selle ajaprobleemiga tegelema, kuna tekstiväli on lahti kärbitud ja taotlus võtab natuke aega.

kirjelda ('otsimisel', () => {
    lase timeInSyncKeeper;
    beforeEach (() => {
        timeInSyncKeeper = uus AsyncZoneTimeInSyncKeeper ();
        spyOn (apiService, 'query') ja.returnValue (Observable.of (queryResult) .delay (REQUEST_DELAY));
    });
    see ('kustutab eelmise tulemuse', fakeAsync (() => {
        comp.options = ['mitte tühi'];
        SpecUtils.focusAndInput ('Lon', kinnitus, 'input');
        timeInSyncKeeper.tick (DEBOUNCING_VALUE);
        fixture.detectChanges ();
        oodata (comp.options.length) .toBe (0, `oli [$ {comp.options.join (',')}] '));
        timeInSyncKeeper.tick (REQUEST_DELAY);
    }));
    // ...
});

Vaatame läbi selle näite ridade kaupa:

  1. koheselt sünkroonimise pidaja eksemplari
timeInSyncKeeper = uus AsyncZoneTimeInSyncKeeper ();

2. laske reageerida meetodile apiService.query tulemuse queryResult abil pärast REQUEST_DELAY möödumist. Oletame, et päringumeetod on aeglane ja vastab pärast REQUEST_DELAY = 5000 millisekundit.

spyOn (apiService, 'query') ja.returnValue (Observable.of (queryResult) .delay (REQUEST_DELAY));

3. Teeskle, et soovitusväljal on valik „mitte tühi”

comp.options = ['mitte tühi'];

4. Minge võistluskalendri loomuliku elemendi väljale „sisend“ ja sisestage väärtus „Lon“. See simuleerib kasutaja suhtlemist sisestusväljaga.

SpecUtils.focusAndInput ('Lon', kinnitus, 'input');

5. laske DEBOUNCING_VALUE ajaperioodil võltsasünkli tsoonis (DEBOUNCING_VALUE = 300 millisekundit).

timeInSyncKeeper.tick (DEBOUNCING_VALUE);

6. Tuvastage muudatused ja muutke HTML-mall uuesti.

fixture.detectChanges ();

7. Valikute massiiv on praegu tühi!

oodata (comp.options.length) .toBe (0, `oli [$ {comp.options.join (',')}] '));

See tähendab, et komponentide jälgitav väärtusmuudatused suudeti õigel ajal käivitada. Pange tähele, et täidetud funktsioon debounceTime-d

väärtus => {
    see.valikud = [];
    this.onEvent.emit ({signaal: SuggestSignal.start});
    this.suggest (väärtus);
}

lükkas järjekordse ülesande järjekorda, nimetades meetodit soovitama:

soovita (q: string) {
    if (! q) {
        tagasi;
    }
    this.googleBooksAPI.query (q) .subscribe (tulemus => {
        if (tulemus) {
            this.options = tulemus.items.map (item => item.volumeInfo);
            this.onEvent.emit ({signaal: SuggestSignal.success, totalItems: tulemus.totalItems});
        } veel {
            this.onEvent.emit ({signaal: SuggestSignal.success, totalItems: 0});
        }
    }, () => {
        this.onEvent.emit ({signaal: SuggestSignal.error});
    });
}

Tuletage vaid meelde google Books API päringumeetodi spioon, mis reageerib 5 sekundi pärast.

8. Lõpuks peame tsoonijärjekorra loputamiseks uuesti tegema valiku REQUEST_DELAY = 5000 millisekundit. Vaatlus, mille tellime soovitamismeetodi abil, vajab lõpuleviimiseks REQUEST_DELAY = 5000.

timeInSyncKeeper.tick (REQUEST_DELAY);

fakeAsync…? Miks? Seal on planeerijad!

ReactiveX-i eksperdid võivad väita, et vaatluste testitavuse muutmiseks võiksime kasutada testimise planeerijaid. See on võimalik nurgeliste rakenduste jaoks, kuid sellel on mõned puudused:

  • see nõuab tutvumist vaatlejate, operaatorite sisemise struktuuriga…
  • mis siis, kui teie rakenduses on mõni kole setTimeout lahendus? Planeerijad ei tegele nendega.
  • kõige olulisem: olen kindel, et te ei soovi kogu ajarakenduses kasutada planeerijaid. Te ei soovi segada tootmiskoodi oma ühikatestidega. Te ei soovi midagi sellist teha:
const testScheduler;
if (keskkond.test) {
    testScheduler = uus YourTestScheduler ();
}
lase nähtavaks;
if (testScheduler) {
    jälgitav = jälgitav.of ('väärtus'). viivitus (1000, testScheduler)
} veel {
    jälgitav = jälgitav.of ('väärtus'). viivitus (1000);
}

See ei ole elujõuline lahendus. Minu arvates on ainus võimalik lahendus testi planeerija "süstimine", pakkudes reaalsete Rxjs-meetodite jaoks teatud tüüpi "puhverserve". Teine asi, mida tuleb arvestada, on see, et ülimuslikud meetodid võivad ülejäänud ühikteste negatiivselt mõjutada. Sellepärast hakkame kasutama Jasmine'i spioone. Spioonid puhastatakse pärast seda.

Funktsioon monkeypatchScheduler märab spiooni abil algse Rxjsi teostuse. Spioon võtab meetodi argumendid ja lisab vajaduse korral testScheduleri.

impordi {IScheduler} kaustast 'rxjs / Scheduler';
import {Observable} kaustast 'rxjs / Observable';
kuulutada var spyOn: funktsioon;
ekspordifunktsioon monkeypatchScheduler (ajakava: IScheduler) {
    lase observableMethods = ['concat', 'edasi lükata', 'tühi', 'forkJoin', 'if', 'intervall', 'ühendada', 'of', 'range', 'visata',
        'tõmblukk'];
    lase operatorMethods = ['puhver', 'concat', 'viivitus', 'selge', 'teha', 'iga', 'viimane', 'ühendada', 'max', 'võtta',
        'timeInterval', 'lift', 'debounceTime'];
    lase injectFn = function (alus: ükskõik, meetodid: string []) {
        method.forEach (meetod => {
            const orig = alus [meetod];
            if (typeof orig === 'function') {
                spyOn (alus, meetod) .and.callFake (function () {
                    las args = Array.prototype.slice.call (argumendid);
                    if (args [args.length - 1] && typeof args [args.length - 1] .now === 'function') {
                        args [args.length - 1] = ajakava;
                    } veel {
                        args.push (planeerija);
                    }
                    tagasi orig.apply (see, args);
                });
            }
        });
    };
    injectFn (jälgitav, jälgitav meetod);
    injectFn (jälgitav prototüüp, operaatori meetodid);
}

Nüüdsest viib testScheduler kogu töö Rxjs-is läbi. See ei kasuta setTimeout / setInterval ega mingeid asünkroonseid asju. FakeAsync pole enam vajalik.

Nüüd on vaja testimise planeerija eksemplari, mille tahame edastada monkeypatchSchedulerile.

See käitub väga sarnaselt vaikimisi seatud TestScheduleriga, kuid pakub tagasihelistamise meetodit onAction. Nii teame, milline toiming pärast millist ajavahemikku teostati.

ekspordiklass SpyingTestScheduler laiendab VirtualTimeScheduleri {
    spyFn: (actionName: string, viivitus: arv, viga ?: mõni) => tühine;
    ehitaja () {
        super (VirtualAction, defaultMaxFrame);
    }
    onAction (spyFn: (actionName: string, viivitus: arv, viga ?: mõni) => tühine) {
        this.spyFn = spyFn;
    }
    loputa () {
        const {tegevused, maxFrames} = see;
        las viga: suvaline, toiming: AsyncAction ;
        while ((action = action.shift ()) && (this.frame = action.delay) <= maxFrames) {
            lase stateName = this.detectStateName (tegevus);
            lase viivitada = action.delay;
            if (viga = action.execute (action.state, action.delay)) {
                if (this.spyFn) {
                    this.spyFn (stateName, viivitus, tõrge);
                }
                vaheaeg;
            } veel {
                if (this.spyFn) {
                    this.spyFn (stateName, viivitus);
                }
            }
        }
        if (viga) {
            while (tegevus = action.shift ()) {
                action.uns tellida ();
            }
            viske viga;
        }
    }
    private deteStateName (tegevus: AsyncAction ): string {
        const c = Object.getPrototypeOf (action.state) .constructor;
        const argsPos = c.toString (). indexOf ('(');
        if (argsPos! == -1) {
            tagasi c.toString (). alamstring (9, argsPos);
        }
        tagasi null;
    }
}

Lõpuks vaatame kasutamist. Näide on sama ühiku test, mida varem kasutatud (see ('kustutab eelmise tulemuse'), väikese erinevusega, et hakkame fakeAsync / linnukese asemel kasutama testimise ajastajat).

lase testScheduler;
beforeEach (() => {
    testScheduler = uus SpyingTestScheduler ();
    testScheduler.maxFrames = 1000000;
    monkeypatchScheduler (testScheduler);
    fixture.detectChanges ();
});
beforeEach (() => {
    spyOn (apiService, 'päring') ja.callFake (() => {
        tagastama Observable.of (queryResult) .delay (REQUEST_DELAY);
    });
});
it ('kustutab eelmise tulemuse', (tehtud: funktsioon) => {
    comp.options = ['mitte tühi'];
    testScheduler.onAction ((actionName: string, viivitus: arv, ekslik ?: mõni) = = {
        if (actionName === 'DebounceTimeSubscriber' && viivitus === DEBOUNCING_VALUE) {
            oodata (comp.options.length) .toBe (0, `oli [$ {comp.options.join (',')}] '));
            tehtud ();
        }
    });
    SpecUtils.focusAndInput ('Londo', kinnitus, 'sisend');
    fixture.detectChanges ();
    testScheduler.flush ();
});

Testi ajakava luuakse ja monkeypatched (!) Esimeses enne iga. Teises, enne igaüks, luurame apiService.query, et tulemust queryResult teenida pärast REQUEST_DELAY = 5000 millisekundit.

Vaatame selle ridahaaval läbi:

  1. Kõigepealt pange tähele, et kuulutame funktsiooni lõpetatuks, mida vajame koos testide ajastaja tagasihelistamise funktsiooniga. See tähendab, et peame Jasmine'ile ütlema, et test tehakse iseseisvalt.
it ('kustutab eelmise tulemuse', (tehtud: funktsioon) => {

2. Jällegi kujutame ette mõned komponendis olevad võimalused.

comp.options = ['mitte tühi'];

3. See nõuab teatavat selgitust, kuna see tundub esmapilgul pisut kohmakas. Tahame oodata toimingut nimega “DebounceTimeSubscriber” viivitusega DEBOUNCING_VALUE = 300 millisekundit. Kui see juhtub, tahame kontrollida, kas options.length on 0. Siis test on lõpule viidud ja kutsume tehtud ().

testScheduler.onAction ((actionName: string, viivitus: arv, ekslik ?: mõni) = = {
    if (actionName === 'DebounceTimeSubscriber' && viivitus === DEBOUNCING_VALUE) {
      oodata (comp.options.length) .toBe (0, `oli [$ {comp.options.join (',')}] '));
      tehtud ();
    }
});

Näete, et testide ajastajate kasutamine nõuab erilisi teadmisi Rxjs-rakenduse sisemiste kohta. Muidugi sõltub see, millist testimise ajastajat te kasutate, kuid isegi kui rakendate võimsa ajakava üksi, peate mõistma ajakavasid ja paljastama paindlikkuse mõned käitusaja väärtused (mis jällegi ei pruugi olla iseenesestmõistetavad).

4. Kasutaja sisestab jällegi väärtuse „Londo“.

SpecUtils.focusAndInput ('Londo', kinnitus, 'sisend');

5. Jällegi tuvastage muudatused ja muutke mall uuesti.

fixture.detectChanges ();

6. Lõpuks teostame kõik toimingud, mis on paigutatud planeerija järjekorda.

testScheduler.flush ();

Kokkuvõte

Nurga enda testimisutiliidid on eelistatavad omavalmistatud utiliididele ... kui need töötavad. Mõnel juhul ei tööta fakeAsync / linnuke paar, kuid pole põhjust meeleheiteks ja ühiskatsete tegemata jätmiseks. Nendel juhtudel on võimalik kasutada automaatse sünkroonimise utiliiti (siin tuntud ka kui AsyncZoneTimeInSyncKeeper) või kohandatud testimise ajastajat (siin tuntud ka kui SpyingTestScheduler).

Lähtekood