Come scrivere test unitari per Angular / TypeScript per metodi privati ​​con Jasmine


198

Come testare una funzione privata in Angular 2?

class FooBar {

    private _status: number;

    constructor( private foo : Bar ) {
        this.initFooBar();

    }

    private initFooBar(){
        this.foo.bar( "data" );
        this._status = this.fooo.foo();
    }

    public get status(){
        return this._status;
    }

}

La soluzione che ho trovato

  1. Inserire il codice di test stesso all'interno della chiusura o Aggiungi codice all'interno della chiusura che memorizza i riferimenti alle variabili locali su oggetti esistenti nell'ambito esterno.

    Successivamente rimuovere il codice di test utilizzando uno strumento. http://philipwalton.com/articles/how-to-unit-test-private-functions-in-javascript/

Per favore, suggeriscimi un modo migliore per risolvere questo problema se ne hai fatto uno?

PS

  1. La maggior parte della risposta per questo tipo di domanda simile a questa non fornisce una soluzione al problema, ecco perché sto ponendo questa domanda

  2. La maggior parte degli sviluppatori afferma di non testare le funzioni private ma non dico che sono sbagliate o giuste, ma ci sono necessità per il mio caso di testare private.


11
test dovrebbe testare solo l'interfaccia pubblica, non l'implementazione privata. I test che fai sull'interfaccia pubblica dovrebbero coprire anche la parte privata.
toskv

16
Mi piace come metà delle risposte dovrebbe effettivamente essere un commento. OP fa una domanda, come si fa X? La risposta accettata in realtà ti dice come fare X. Quindi la maggior parte del resto si gira e dice, non solo non ti dirò X (che è chiaramente possibile) ma dovresti fare Y. La maggior parte degli strumenti di test delle unità (non lo sono parlando solo di JavaScript qui) sono in grado di testare funzioni / metodi privati. Continuerò a spiegare perché perché sembra essersi perso nella terra di JS (apparentemente, dato la metà delle risposte).
Quaternion,

13
È buona prassi di programmazione suddividere un problema in attività gestibili, quindi la funzione "foo (x: type)" chiamerà le funzioni private a (x: type), b (x: type), c (y: another_type) e d ( z: yet_another_type). Ora perché foo, sta gestendo le chiamate e mettendo insieme le cose, crea una sorta di turbolenza, come i lati posteriori delle rocce in un ruscello, ombre che sono davvero difficili da garantire che tutte le gamme siano testate. Pertanto, è più semplice garantire che ogni sottoinsieme di intervalli sia valido, se si tenta di testare il "pippo" da solo il test dei range diventa molto complicato in alcuni casi.
Quaternion,

18
Questo non vuol dire che non testare l'interfaccia pubblica, ovviamente lo fai, ma testare i metodi privati ​​ti consente di testare una serie di brevi blocchi gestibili (lo stesso motivo per cui li hai scritti in primo luogo, perché dovresti annullare questo quando si tratta di test), e solo perché i test su interfacce pubbliche sono validi (forse la funzione di chiamata limita gli intervalli di input) non significa che i metodi privati ​​non siano imperfetti quando aggiungi logica più avanzata e li chiami da altri nuove funzioni genitore,
Quaternion,

5
se li hai testati correttamente con TDD non proverai a capire cosa diavolo stavi facendo in seguito, quando avresti dovuto testarli correttamente.
Quaternion,

Risposte:


348

Sono con te, anche se è un buon obiettivo "testare solo unità l'API pubblica" ci sono momenti in cui non sembra così semplice e ritieni di scegliere tra compromettere l'API o i test unitari. Lo sai già, dato che è esattamente quello che stai chiedendo di fare, quindi non ci entrerò. :)

In TypeScript ho scoperto alcuni modi in cui puoi accedere a membri privati ​​per motivi di unit test. Considera questa classe:

class MyThing {

    private _name:string;
    private _count:number;

    constructor() {
        this.init("Test", 123);
    }

    private init(name:string, count:number){
        this._name = name;
        this._count = count;
    }

    public get name(){ return this._name; }

    public get count(){ return this._count; }

}

Anche se TS limita l'accesso ai membri della classe utilizzando private, protected, public, il JS compilato non ha membri privati, dal momento che questa non è una cosa in JS. È utilizzato esclusivamente per il compilatore TS. Perciò:

  1. Puoi affermare anye sfuggire al compilatore di avvisarti delle restrizioni di accesso:

    (thing as any)._name = "Unit Test";
    (thing as any)._count = 123;
    (thing as any).init("Unit Test", 123);
    

    Il problema con questo approccio è che il compilatore semplicemente non ha idea di cosa stai facendo proprio any, quindi non ottieni gli errori di tipo desiderati:

    (thing as any)._name = 123; // wrong, but no error
    (thing as any)._count = "Unit Test"; // wrong, but no error
    (thing as any).init(0, "123"); // wrong, but no error
    

    Ciò renderà ovviamente più difficile il refactoring.

  2. È possibile utilizzare l'array access ( []) per accedere ai membri privati:

    thing["_name"] = "Unit Test";
    thing["_count"] = 123;
    thing["init"]("Unit Test", 123);
    

    Anche se sembra funky, TSC convaliderà effettivamente i tipi come se li accedessi direttamente:

    thing["_name"] = 123; // type error
    thing["_count"] = "Unit Test"; // type error
    thing["init"](0, "123"); // argument error
    

    Ad essere sincero, non so perché funzioni. Questo è apparentemente un "trampolino di fuga" intenzionale per darti accesso ai membri privati ​​senza perdere la sicurezza del tipo. Questo è esattamente quello che penso tu voglia per il tuo test unitario.

Ecco un esempio funzionante nel Playground di TypeScript .

Modifica per TypeScript 2.6

Un'altra opzione che alcuni preferiscono utilizzare // @ts-ignore( aggiunta in TS 2.6 ) che elimina semplicemente tutti gli errori nella riga seguente:

// @ts-ignore
thing._name = "Unit Test";

Il problema con questo è, beh, sopprime tutti gli errori sulla seguente riga:

// @ts-ignore
thing._name(123).this.should.NOT.beAllowed("but it is") = window / {};

Personalmente considero @ts-ignoreun odore di codice e come dicono i documenti:

ti consigliamo di usare questi commenti con parsimonia . [enfasi originale]


46
È così bello sentire una posizione realistica sui test unitari insieme a una soluzione reale piuttosto che al dogma del tester unità standard.
d512,

2
Qualche spiegazione "ufficiale" del comportamento (che cita persino il test unitario come caso d'uso): github.com/microsoft/TypeScript/issues/19335
Aaron Beall,

1
Basta usare` // @ ts-ignore` come indicato di seguito. dire alla linter di ignorare l'accessorio privato
Tommaso

1
@ Tommaso Sì, questa è un'altra opzione, ma ha lo stesso svantaggio dell'uso as any: si perde tutto il controllo del tipo.
Aaron Beall,

2
La migliore risposta che ho visto da un po ', grazie @AaronBeall. E anche grazie a tymspy per aver posto la domanda originale.
nicolas.leblanc,

27

Puoi chiamare metodi privati . Se hai riscontrato il seguente errore:

expect(new FooBar(/*...*/).initFooBar()).toEqual(/*...*/);
// TS2341: Property 'initFooBar' is private and only accessible within class 'FooBar'

basta usare // @ts-ignore:

// @ts-ignore
expect(new FooBar(/*...*/).initFooBar()).toEqual(/*...*/);

questo dovrebbe essere al top!
jsnewbie,

2
Questa è sicuramente un'altra opzione. Soffre dello stesso problema as anydel fatto che si perde qualsiasi controllo del tipo, in realtà si perde qualsiasi controllo del tipo su tutta la linea.
Aaron Beall il

20

Poiché la maggior parte degli sviluppatori non consiglia di testare la funzione privata , perché non provarla ?.

Per esempio.

YourClass.ts

export class FooBar {
  private _status: number;

  constructor( private foo : Bar ) {
    this.initFooBar({});
  }

  private initFooBar(data){
    this.foo.bar( data );
    this._status = this.foo.foo();
  }
}

TestYourClass.spec.ts

describe("Testing foo bar for status being set", function() {

...

//Variable with type any
let fooBar;

fooBar = new FooBar();

...
//Method 1
//Now this will be visible
fooBar.initFooBar();

//Method 2
//This doesn't require variable with any type
fooBar['initFooBar'](); 
...
}

Grazie a @Aaron, @Thierry Templier.


1
Penso che dattiloscritto dia errori di sfilacciatura quando si tenta di chiamare un metodo privato / protetto.
Gudgip,

1
@Gudgip darebbe errori di tipo e non verranno compilati. :)
tymspy,

10

Non scrivere test per metodi privati. Questo sconfigge il punto dei test unitari.

  • Dovresti testare l'API pubblica della tua classe
  • NON dovresti testare i dettagli dell'implementazione della tua classe

Esempio

class SomeClass {

  public addNumber(a: number, b: number) {
      return a + b;
  }
}

Il test per questo metodo non dovrebbe cambiare se successivamente l'implementazione cambia ma l' behaviourAPI pubblica rimane la stessa.

class SomeClass {

  public addNumber(a: number, b: number) {
      return this.add(a, b);
  }

  private add(a: number, b: number) {
       return a + b;
  }
}

Non rendere pubblici metodi e proprietà solo per testarli. Questo di solito significa che:

  1. Stai provando a testare l'implementazione anziché l'API (interfaccia pubblica).
  2. È necessario spostare la logica in questione nella propria classe per semplificare i test.

3
Magari leggi il post prima di commentarlo. Dichiaro chiaramente e dimostriamo che testare i privati ​​è un odore dell'implementazione dei test piuttosto che del comportamento, che porta a test fragili.
Martin,

1
Immagina un oggetto che ti dia un numero casuale compreso tra 0 e la proprietà privata x. Se vuoi sapere se x è impostato correttamente dal costruttore, è molto più semplice testare il valore di x piuttosto che effettuare un centinaio di test per verificare se i numeri che ottieni sono nel giusto intervallo.
Galdor,

1
@ user3725805 questo è un esempio di test dell'implementazione, non del comportamento. Sarebbe meglio isolare da dove proviene il numero privato: una costante, una configurazione, un costruttore - e test da lì. Se il privato non proviene da qualche altra fonte, allora cade nell'antipasto "numero magico".
Martin,

1
E perché non è consentito testare l'implementazione? I test unitari sono utili per rilevare cambiamenti imprevisti. Quando per qualche motivo il costruttore dimentica di impostare il numero, il test fallisce immediatamente e mi avvisa. Quando qualcuno modifica l'implementazione, anche il test fallisce, ma preferisco adottare un test piuttosto che avere un errore non rilevato.
Galdor,

2
+1. Bella risposta. @TimJames Raccontare la pratica corretta o sottolineare l'approccio difettoso è lo scopo stesso di SO. Invece di trovare un modo fragile per ottenere ciò che l'OP vuole.
Syed Aqeel Ashiq

4

Il punto di "non testare metodi privati" è davvero Testare la classe come qualcuno che la utilizza .

Se disponi di un'API pubblica con 5 metodi, qualsiasi consumatore della tua classe può utilizzarli e quindi dovresti testarli. Un consumatore non dovrebbe accedere ai metodi / proprietà privati ​​della tua classe, il che significa che puoi cambiare membri privati ​​quando la funzionalità esposta al pubblico rimane invariata.


Se si fa affidamento su funzionalità estensibili interne, utilizzare protectedinvece di private.
Nota che protectedè ancora un'API pubblica (!) , Utilizzata solo in modo diverso.

class OverlyComplicatedCalculator {
    public add(...numbers: number[]): number {
        return this.calculate((a, b) => a + b, numbers);
    }
    // can't be used or tested via ".calculate()", but it is still part of your public API!
    protected calculate(operation, operands) {
        let result = operands[0];
        for (let i = 1; i < operands.length; operands++) {
            result = operation(result, operands[i]);
        }
        return result;
    }
}

Proprietà protette unit test nello stesso modo in cui un consumatore le userebbe, tramite la sottoclasse:

it('should be extensible via calculate()', () => {
    class TestCalculator extends OverlyComplicatedCalculator {
        public testWithArrays(array: any[]): any[] {
            const concat = (a, b) => [].concat(a, b);
            // tests the protected method
            return this.calculate(concat, array);
        }
    }
    let testCalc = new TestCalculator();
    let result = testCalc.testWithArrays([1, 'two', 3]);
    expect(result).toEqual([1, 'two', 3]);
});

3

Questo ha funzionato per me:

Invece di:

sut.myPrivateMethod();

Questo:

sut['myPrivateMethod']();

2

Ci scusiamo per il necro in questo post, ma mi sento in dovere di ponderare un paio di cose che non sembrano essere state toccate.

Innanzitutto - quando ci troviamo a dover accedere a membri privati ​​in una classe durante i test unitari, è generalmente una grande e grossa bandiera rossa che abbiamo imbrogliato nel nostro approccio strategico o tattico e abbiamo inavvertitamente violato il singolo responsabile della responsabilità spingendo comportamento a cui non appartiene. Sentire la necessità di accedere a metodi che non sono altro che una subroutine isolata di una procedura di costruzione è uno degli eventi più comuni di questo; tuttavia, è un po 'come se il tuo capo si aspettasse che ti presentassi per un lavoro pronto e che avesse anche qualche perverso bisogno di sapere quale routine mattutina hai passato per portarti in quello stato ...

L'altro caso più comune di questo evento è quando ti ritrovi a provare la proverbiale "classe del dio". Si tratta di un tipo speciale di problema in sé e per sé, ma soffre dello stesso problema di base con la necessità di conoscere i dettagli intimi di una procedura, ma questo è fuori tema.

In questo esempio specifico, abbiamo effettivamente assegnato la responsabilità di inizializzare completamente l'oggetto Bar al costruttore della classe FooBar. Nella programmazione orientata agli oggetti, uno dei principali inquilini è che il costruttore è "sacro" e dovrebbe essere protetto da dati non validi che invaliderebbero il proprio stato interno e lo lascerebbero innescato per fallire altrove a valle (in quello che potrebbe essere un profondo tubatura.)

Non siamo riusciti a farlo qui consentendo all'oggetto FooBar di accettare una barra che non è pronta al momento della costruzione del FooBar e che abbiamo compensato con una sorta di "hacking" dell'oggetto FooBar per prendere le cose in proprio mani.

Questo è il risultato di una mancata adesione a un altro tenente di programmazione orientata agli oggetti (nel caso di Bar,) che è che lo stato di un oggetto dovrebbe essere completamente inizializzato e pronto a gestire qualsiasi chiamata in entrata verso i suoi "membri pubblici immediatamente dopo la creazione. Ora, questo non significa immediatamente dopo che il costruttore è stato chiamato in tutte le istanze. Quando si dispone di un oggetto con molti scenari di costruzione complessi, è meglio esporre i setter ai suoi membri opzionali a un oggetto che viene implementato secondo un modello di progettazione della creazione (Factory, Builder, ecc ...) questi ultimi casi,

Nel tuo esempio, la proprietà "status" della barra non sembra essere in uno stato valido in cui un FooBar può accettarlo, quindi FooBar fa qualcosa per correggerlo.

Il secondo problema che sto vedendo è che sembra che tu stia provando a testare il tuo codice piuttosto che praticare lo sviluppo guidato dai test. Questa è sicuramente la mia opinione in questo momento; ma, questo tipo di test è davvero un anti-pattern. Quello che finisci per fare è cadere nella trappola della consapevolezza di avere problemi di progettazione fondamentali che impediscono al codice di essere testabile dopo il fatto, piuttosto che scrivere i test necessari e successivamente programmarli. In entrambi i casi, dovresti comunque ottenere lo stesso numero di test e righe di codice se avessi davvero raggiunto un'implementazione SOLID. Quindi - perché provare a decodificare la tua strada nel codice testabile quando puoi semplicemente affrontare la questione all'inizio dei tuoi sforzi di sviluppo?

Se lo avessi fatto, allora ti saresti reso conto molto prima che avresti dovuto scrivere un po 'di codice piuttosto complicato per testare il tuo design e avresti avuto presto l'opportunità di riallineare il tuo approccio spostando il comportamento su implementazioni che sono facilmente testabili.


2

Sono d'accordo con @toskv: non consiglierei di farlo :-)

Ma se vuoi davvero testare il tuo metodo privato, puoi essere consapevole che il codice corrispondente per TypeScript corrisponde a un metodo del prototipo della funzione di costruzione. Ciò significa che può essere utilizzato in fase di esecuzione (mentre probabilmente si verificheranno alcuni errori di compilazione).

Per esempio:

export class FooBar {
  private _status: number;

  constructor( private foo : Bar ) {
    this.initFooBar({});
  }

  private initFooBar(data){
    this.foo.bar( data );
    this._status = this.foo.foo();
  }
}

sarà traspilato in:

(function(System) {(function(__moduleName){System.register([], function(exports_1, context_1) {
  "use strict";
  var __moduleName = context_1 && context_1.id;
  var FooBar;
  return {
    setters:[],
    execute: function() {
      FooBar = (function () {
        function FooBar(foo) {
          this.foo = foo;
          this.initFooBar({});
        }
        FooBar.prototype.initFooBar = function (data) {
          this.foo.bar(data);
          this._status = this.foo.foo();
        };
        return FooBar;
      }());
      exports_1("FooBar", FooBar);
    }
  }
})(System);

Vedi questo plunkr: https://plnkr.co/edit/calJCF?p=preview .


1

Come molti hanno già affermato, per quanto tu voglia testare i metodi privati ​​non dovresti hackerare il tuo codice o transpiler per farlo funzionare per te. Il moderno TypeScript negherà la maggior parte degli hack che le persone hanno fornito finora.


Soluzione

TLDR ; se un metodo deve essere testato, è necessario disaccoppiare il codice in una classe in modo da poter esporre il metodo ad essere pubblico da testare.

Il motivo per cui hai il metodo privato è perché la funzionalità non appartiene necessariamente ad essere esposta da quella classe, e quindi se la funzionalità non appartiene lì dovrebbe essere disaccoppiata nella sua classe.

Esempio

Mi sono imbattuto in questo articolo che fa un ottimo lavoro nel spiegare come dovresti affrontare i test sui metodi privati. Copre anche alcuni dei metodi qui e come perché sono cattive implementazioni.

https://patrickdesjardins.com/blog/how-to-unit-test-private-method-in-typescript-part-2

Nota : questo codice viene estratto dal blog collegato sopra (sto duplicando nel caso in cui il contenuto dietro il collegamento cambi)

Prima
class User{
    public getUserInformationToDisplay(){
        //...
        this.getUserAddress();
        //...
    }

    private getUserAddress(){
        //...
        this.formatStreet();
        //...
    }
    private formatStreet(){
        //...
    }
}
Dopo
class User{
    private address:Address;
    public getUserInformationToDisplay(){
        //...
        address.getUserAddress();
        //...
    }
}
class Address{
    private format: StreetFormatter;
    public format(){
        //...
        format.ToString();
        //...
    }
}
class StreetFormatter{
    public toString(){
        // ...
    }
}

1

chiama il metodo privato usando parentesi quadre

Ts file

class Calculate{
  private total;
  private add(a: number) {
      return a + total;
  }
}

file spect.ts

it('should return 5 if input 3 and 2', () => {
    component['total'] = 2;
    let result = component['add'](3);
    expect(result).toEqual(5);
});

0

La risposta di Aaron è la migliore e sta funzionando per me :) Vorrei votare ma purtroppo non ci riesco (manca la reputazione).

Devo dire che testare metodi privati ​​è l'unico modo per usarli e avere un codice pulito dall'altra parte.

Per esempio:

class Something {
  save(){
    const data = this.getAllUserData()
    if (this.validate(data))
      this.sendRequest(data)
  }
  private getAllUserData () {...}
  private validate(data) {...}
  private sendRequest(data) {...}
}

Ha molto senso non testare tutti questi metodi contemporaneamente perché avremmo bisogno di deridere quei metodi privati, che non possiamo deridere perché non possiamo accedervi. Ciò significa che abbiamo bisogno di molta configurazione per un unit test per testarlo nel suo insieme.

Ciò ha detto che il modo migliore per testare il metodo sopra con tutte le dipendenze è un test end-to-end, perché qui è necessario un test di integrazione, ma il test E2E non ti aiuterà se stai praticando TDD (Test Driven Development), ma test qualsiasi metodo lo farà.


0

Questo percorso che prendo è quello in cui creo funzioni al di fuori della classe e assegno la funzione al mio metodo privato.

export class MyClass {
  private _myPrivateFunction = someFunctionThatCanBeTested;
}

function someFunctionThatCanBeTested() {
  //This Is Testable
}

Ora non so che tipo di regole OOP sto infrangendo, ma per rispondere alla domanda, ecco come testare i metodi privati. Accolgo con favore chiunque ti consigli su Pro e contro di questo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.