it-swarm.com.de

So schreiben Sie Unit-Tests für Angular 2/TypeScript für private Methoden mit Jasmine

Wie testest du eine private Funktion in Winkel 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;
    }

}

Die Lösung habe ich gefunden 

  1. Fügen Sie den Testcode selbst in den Abschluss ein oder fügen Sie Code in den Abschluss ein, der Verweise auf die lokalen Variablen für vorhandene Objekte im äußeren Bereich speichert. 

    Entfernen Sie später den Testcode mit einem Tool . http://philipwalton.com/articles/how-to-unit-test-private-functions-in-javascript/

Bitte schlagen Sie mir einen besseren Weg vor, um dieses Problem zu lösen, wenn Sie etwas getan haben.

P.S 

  1. Die meisten Antworten auf ähnliche Fragen wie diese geben keine Lösung für das Problem. Deshalb stelle ich diese Frage

  2. Die meisten Entwickler sagen, dass Sie keine privaten Funktionen testen, aber ich sage nicht, dass sie falsch oder richtig sind, aber es ist notwendig, dass mein Fall private testet.

98
tymspy

Ich bin bei Ihnen, auch wenn es ein gutes Ziel ist, "nur die öffentliche API zu testen". Manchmal scheint es jedoch nicht so einfach zu sein, und Sie haben das Gefühl, dass Sie entweder die API oder die Komponententests kompromittieren. Sie wissen das schon, denn genau das wollen Sie tun, damit ich nicht darauf eingehen kann. :)

In TypeScript habe ich einige Möglichkeiten entdeckt, wie Sie auf private Mitglieder zugreifen können, um Unit-Tests durchzuführen. Betrachten Sie diese Klasse:

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

}

Obwohl TS den Zugriff auf Klassenmitglieder mit private, protected, public beschränkt, hat der kompilierte JS keine privaten Mitglieder, da dies in JS keine Rolle spielt. Es wird nur für den TS-Compiler verwendet. Dafür:

  1. Sie können any behaupten und den Compiler davon abhalten, Sie vor Zugriffsbeschränkungen zu warnen:

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

    Das Problem bei diesem Ansatz ist, dass der Compiler einfach keine Ahnung hat, was Sie mit der any richtig machen, so dass Sie keine gewünschten Typfehler erhalten:

    (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
    
  2. Sie können den Array-Zugriff ([]) verwenden, um an die privaten Mitglieder zu gelangen:

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

    TSC prüft die Typen zwar so, als würde man direkt auf sie zugreifen:

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

    Um ehrlich zu sein, weiß ich nicht, warum das funktioniert. Es scheint, dass Array-Klammern keine Zugriffsbeschränkungen erzwingen, aber die Typeninferenz gibt Ihnen die volle Typensicherheit. Das ist genau das, was ich für Ihre Unit-Tests denke.

Hier ist ein Arbeitsbeispiel im TypeScript Playground .

186
Aaron

Da die meisten Entwickler nicht empfehlen, private Funktionen zu testen , Warum nicht? 

Z.B.

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'](); 
...
}

Danke an @Aaron, @Thierry Templier.

16
tymspy

Schreiben Sie keine Tests für private Methoden. Dies beseitigt den Punkt von Komponententests. 

  • Sie sollten die öffentliche API Ihrer Klasse testen
  • Sie sollten die Implementierungsdetails Ihrer Klasse NICHT testen

Beispiel

class SomeClass {

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

Der Test für diese Methode sollte nicht geändert werden müssen, wenn die Implementierung später geändert wird, die behaviour der öffentlichen API jedoch gleich bleibt.

class SomeClass {

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

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

Machen Sie Methoden und Eigenschaften nicht öffentlich, nur um sie zu testen. Dies bedeutet normalerweise, dass entweder

  1. Sie versuchen, die Implementierung anstelle der API (öffentliche Schnittstelle) zu testen.
  2. Sie sollten die betreffende Logik in eine eigene Klasse verschieben, um das Testen zu erleichtern.
7
Martin

Sie können private Methoden aufrufen. Wenn Sie auf folgenden Fehler gestoßen sind:

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

benutze einfach // @ts-ignore:

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

Tut mir leid für die Necro in diesem Beitrag, aber ich fühle mich gezwungen, sich auf ein paar Dinge zu konzentrieren, die anscheinend nicht berührt worden sind.

Zuallererst - wenn wir uns während eines Unit-Tests mit privaten Mitgliedern einer Klasse befassen müssen, ist es in der Regel eine große, fette rote Fahne, die wir in unserem strategischen oder taktischen Ansatz vermasselt haben und unabsichtlich das Prinzip der einzelnen Verantwortlichkeit durch Pushen verletzt haben Verhalten, wo es nicht hingehört. Die Notwendigkeit, auf Methoden zuzugreifen, die eigentlich nichts anderes als eine isolierte Subroutine einer Konstruktionsprozedur sind, ist eines der häufigsten Vorkommnisse; Es ist jedoch so, als würde Ihr Chef erwarten, dass Sie zur Arbeit bereit sind und ein perverses Bedürfnis haben, zu wissen, welche Morgenroutine Sie durchgemacht haben, um Sie in diesen Zustand zu bringen ...

Das andere häufigste Beispiel ist, dass Sie versuchen, die sprichwörtliche "Gott-Klasse" zu testen. Es ist ein spezielles Problem an und für sich, aber es besteht das gleiche grundlegende Problem, dass Sie die genauen Details einer Prozedur kennen müssen.

In diesem speziellen Beispiel haben wir die Verantwortung für die vollständige Initialisierung des Bar-Objekts dem Konstruktor der Klasse FooBar zugewiesen. Bei der objektorientierten Programmierung ist einer der Kernpunkte, dass der Konstruktor "heilig" ist und vor ungültigen Daten geschützt werden sollte, die den eigenen internen Zustand ungültig machen und ihn dazu veranlassen, an einer anderen Stelle stromabwärts zu versagen (was sehr tief sein könnte) Pipeline.) 

Wir haben das hier nicht geschafft, indem wir zulassen, dass das FooBar-Objekt eine Bar akzeptiert, die zum Zeitpunkt der Erstellung der FooBar noch nicht fertig ist, und dass wir das FooBar-Objekt durch "Hacken" kompensieren, um die Dinge in die eigene Hand zu nehmen Hände. 

Dies ist das Ergebnis eines Versäumnisses, sich an einen anderen Tenent der objektorientierten Programmierung zu halten (im Fall von Bar). Dies bedeutet, dass der Zustand eines Objekts vollständig initialisiert werden sollte und bereit ist, eingehende Anrufe an seine öffentlichen Mitglieder sofort nach der Erstellung zu bearbeiten. Dies bedeutet jetzt nicht unmittelbar nach dem Aufruf des Konstruktors in allen Fällen. Wenn Sie ein Objekt mit vielen komplexen Konstruktionsszenarien haben, empfiehlt es sich, Setter für seine optionalen Member einem Objekt auszusetzen, das gemäß einem Erstellungsdesignmuster (Factory, Builder usw.) implementiert ist In letzteren Fällen würden Sie die Initialisierung des Zielobjekts in einen anderen Objektgraphen verschieben, dessen einziger Zweck darin besteht, den Datenverkehr zu einem Punkt zu führen, an dem Sie eine gültige Instanz des gewünschten Objekts haben - und das Produkt nicht sein sollte als "fertig" betrachtet, bis dieses Erstellungsobjekt es bedient hat. 

In Ihrem Beispiel scheint die "status" -Eigenschaft der Bar nicht in einem gültigen Zustand zu sein, in dem eine FooBar sie akzeptieren kann. Daher tut die FooBar etwas, um dieses Problem zu beheben. 

Das zweite Problem, das ich sehe, ist, dass es scheint, dass Sie versuchen, Ihren Code zu testen, anstatt testgetriebene Entwicklung zu üben. Dies ist definitiv meine eigene Meinung zu diesem Zeitpunkt; Aber diese Art von Tests ist wirklich ein Anti-Muster. Was Sie am Ende tun, ist die Falle, dass Sie feststellen, dass Sie grundlegende Designprobleme haben, die verhindern, dass Ihr Code nachträglich getestet werden kann, anstatt die benötigten Tests zu schreiben und anschließend die Tests zu programmieren. Wie dem auch sei, wenn Sie das Problem lösen, sollten Sie trotzdem die gleiche Anzahl von Tests und Codezeilen erhalten, wenn Sie wirklich eine SOLID -Implementierung durchgeführt hätten. Warum sollten Sie also versuchen, Ihren Weg zurück zu testbarem Code zu entwickeln, wenn Sie sich zu Beginn Ihrer Entwicklungsanstrengungen mit der Angelegenheit befassen können? 

Wenn Sie das getan hätten, hätten Sie viel früher gemerkt, dass Sie etwas eckigen Code schreiben müssen, um mit Ihrem Design zu testen, und hätte die Gelegenheit gehabt, Ihren Ansatz neu auszurichten, indem Sie das Verhalten auf Implementierungen umstellen sind leicht überprüfbar. 

2
Ryan Hansen

Der Sinn von "private Methoden nicht testen" ist wirklich Testen Sie die Klasse wie jemand, der sie verwendet.

Wenn Sie über eine öffentliche API mit 5 Methoden verfügen, kann jeder Konsument Ihrer Klasse diese verwenden. Daher sollten Sie sie testen. Ein Consumer sollte nicht auf die privaten Methoden/Eigenschaften Ihrer Klasse zugreifen. Das heißt, Sie können private Mitglieder ändern, wenn die öffentlich verfügbaren Funktionen gleich bleiben.


Wenn Sie sich auf interne erweiterbare Funktionen verlassen, verwenden Sie protected anstelle von private.
Beachten Sie, dass protected immer noch eine public-API (!) ist, die gerade anders verwendet wird.

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

Einheitentest-geschützte Eigenschaften auf dieselbe Weise wie ein Verbraucher sie mittels Unterklassifizierung verwenden würde:

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]);
});
2
Leon Adler

Ich stimme @toskv zu: Ich würde nicht empfehlen, dass :-)

Wenn Sie jedoch Ihre private Methode wirklich testen möchten, können Sie wissen, dass der entsprechende Code für das TypeScript einer Methode des Konstruktorfunktionsprototyps entspricht. Dies bedeutet, dass es zur Laufzeit verwendet werden kann (wobei Sie wahrscheinlich einige Kompilierungsfehler haben werden).

Zum Beispiel:

export class FooBar {
  private _status: number;

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

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

wird übersetzt 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);

Siehe diesen Plan: https://plnkr.co/edit/calJCF?p=preview .

1

Auf dieser Route erstelle ich Funktionen außerhalb der Klasse und ordne die Funktion meiner privaten Methode zu. 

export class MyClass {
  private _myPrivateFunction = someFunctionThatCanBeTested;
}

function someFunctionThatCanBeTested() {
  //This Is Testable
}

Nun weiß ich nicht, welche Art von OOP Regeln ich brich, aber um die Frage zu beantworten, teste ich private Methoden. Ich begrüße jeden, der diesbezüglich über Pros & Cons beraten wird. 

0
Sani Yusuf

Die Antwort von Aaron ist die beste und arbeitet für mich:) Ich würde es abwählen, aber leider kann ich es nicht (fehlender Ruf).

Ich muss sagen, dass das Testen privater Methoden die einzige Möglichkeit ist, sie zu verwenden und auf der anderen Seite sauberen Code zu verwenden.

Zum Beispiel:

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

Es ist sehr sinnvoll, nicht alle diese Methoden gleichzeitig zu testen, da wir diese privaten Methoden ausspionieren müssten, was wir nicht ausmachen können, weil wir nicht auf sie zugreifen können. Dies bedeutet, dass wir für einen Komponententest viel Konfiguration benötigen, um dies als Ganzes zu testen.

Der beste Weg, um die obige Methode mit allen Abhängigkeiten zu testen, ist ein Ende-zu-Ende-Test, da hier ein Integrationstest erforderlich ist. Der E2E-Test hilft Ihnen jedoch nicht, wenn Sie TDD (Test Driven Development) üben, aber testen Jede Methode wird. 

0
Devpool

Als Option können Sie protected statt privat verwenden und in Ihrem Test "test" als Kind deklarieren.

class FooBar {

    private _status: number;

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

    }

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

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

}

in Tests:

export class FooBarTest extends FooBar {
  ...
  initFooBar() {
    return super.initFooBar();
  }
  ...
}

...

it('initFooBar test', () => {
    const fooBarTest = new FooBarTest();
    fooBarTest.initFooBar();
    expect(fooBarTest.status).toEqual('data matata');
  });
...