javascript - Sinon - Cum să stub-uiesc o metodă apelată de metoda pe care vreau să o testez


Am probleme să creez un stub pentru o metodă care este folosită de o metodă pe care vreau să o testez în TypeScript. Am eliminat o mare parte din metoda în sine în exemplu pentru claritate, dar practic am o metodă getServiceWithRetry care apelează metoda getService.

ie.

export function getServiceWithRetry(name:string, triesLeft:number) {
    //do stuff
    getService(name)
    //do more stuff
}

export function getService(name:string) {
    //lookup stuff
}

Acesta este importat în testul meu ca Lookup. Pot să stub‑uiesc cu succes metoda getService dacă apelez getService în test, totuși când rulez getServiceWithRetry aceasta apelează metoda reală getService și nu stub-ul. Știe cineva ce greșesc?

it("test", function(done) {
    let serviceStub = sinon.stub(Lookup, 'getService')

    serviceStub.returns(Promise.resolve("resolved"))

    //this uses the stub
    Lookup.getService("name").then(function(value) {
        console.log("success: "+value)
    }, function(error) {
        console.log("error: "+error)
    })

    //this calls the actual method, not the stub as I would expect it to
    Lookup.getServiceWithRetry("serviceName", 4).then(function(value) {
        console.log("success: "+value)
    }, function(error) {
        console.log("error: "+error)
    })
    done()
})

Notă: Pentru cei care nu sunt familiarizați cu promisiunile bluebird, metoda .then(function(value){}, function(error){}) gestionează ce se întâmplă dacă promisiunea este îndeplinită cu succes și dacă promisiunea este respinsă.

Răspuns #1

Trebuie să schimbi:

export function getServiceWithRetry(name:string, triesLeft:number) {
    //do stuff
    getService(name)
    //do more stuff
}

către :

export function getServiceWithRetry(name:string, triesLeft:number) {
    //do stuff
    this.getService(name)
    //do more stuff
}

în acest fel, când apelezi Lookup.getServiceWithRetry(), apelul getService() va indica către Lookup.getService() și nu către getService() care se află în modulul pe care îl exporți.

Răspunsul #2

Problema este că cu sinon.stub(Lookup, 'getService') modificați componentele interne ale variabilei Lookup pe care o dețineți în testele voastre și apoi obțineți metoda din acea variabilă. În modulul vostru Lookup însă funcția găsește pur și simplu getService direct din domeniul său local. Extern, nu cred că există vreo modalitate prin care puteți interveni asupra acelui domeniu, așa că mă tem că nu există o soluție magică ușoară pentru asta.

În general, de obicei nu poți să faci o simulare amabilă a unor părți ale unui singur modul într-un test. Trebuie să restructurezi puțin asta, și există câteva opțiuni:

  • Testează-le complet separat. Transformă getServiceWithRetry într-o metodă generică retry, de exemplu astfel încât să o poți apela ca retry(nTimes, getService, "serviceName") sau retry(() => getService("serviceName"), nTimes)). Dacă este practic să faci asta (adică dacă nu este prea legat de getService), atunci poți testa cu ușurință acest lucru pe cont propriu:

    var myStub = sinon.stub();
    
    myStub.onCall(0).throw("fail once");
    myStub.onCall(0).throw("fail twice");
    myStub.returns(true); // then return happily
    
    expect(retry(myStub, 1)).to.throw("fail twice"); // gives up after one retry
    expect(retry(myStub, 5)).to.return(true); // keeps going to success
    

    Dacă în altă parte vrei să poți pur și simplu să apelezi un singur getServiceWithRetry, poți construi unul ușor: var getServiceWithRetry=(arg, triesLeft) => retry(getService, tries)

  • Renunță și testează-le împreună. Aceasta înseamnă să înlocuiești cu stub lucrurile de care depinde getService, în loc să îl înlocuiești direct. Depinde de nivelul de granularitate pe care îl dorești pentru testele tale, dar dacă acest cod este simplu și poți testa la un nivel mai grosier, aceasta ar putea fi o opțiune ușoară.

    S-ar putea să doriți să faceți acest lucru chiar dacă le-ați separat oricum, pentru a obține un test de unitate și un test de integrare pentru o acoperire suplimentară. Acest lucru este cu atât mai adevărat dacă există unele interacțiuni mai complicate între ele.

  • Poate nu este relevant în cazul acesta, din câte pot vedea, dar în alte cazuri poți să faci ceva de genul: pune metoda testată (getServiceWithRetry) într-o clasă și folosește injecția de dependență. Ai crea o clasă care primește dependența (metoda getService) în constructor, o stochează intern și o folosește mai târziu când apelezi metode pe obiectul rezultat. În codul de producție, ceva trebuie să le lege corect, iar în testele tale poți folosi în schimb un stub.

  • De asemenea, mă aștept să fie exagerat pentru acest caz, dar ai putea scoate getService într-un modul complet separat pe care Lookup îl importă și să folosești ceva de genul Rewire pentru a-l înlocui cu un alt modul în timpul testării.

    Acest lucru este destul de similar cu opțiunea de injecție a dependențelor într-adevăr și face codul tău de producție mai simplu, dar pe seama faptului că face codul tău de testare mai complicat și magic.