Wie schreibe ich Unit-Tests für Angular / TypeScript für private Methoden mit Jasmine?

196

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, die ich gefunden habe

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

    Entfernen Sie später den Testcode mit einem Werkzeug. 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 welche getan haben.

PS

  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 gibt Notwendigkeiten für meinen Fall, um private Funktionen zu testen.

tymspy
quelle
11
Tests sollten nur die öffentliche Schnittstelle testen, nicht die private Implementierung. Die Tests, die Sie auf der öffentlichen Schnittstelle durchführen, sollten auch den privaten Teil abdecken.
toskv
16
Mir gefällt, wie die Hälfte der Antworten eigentlich Kommentare sein sollten. OP stellt die Frage, wie geht es Ihnen X? Die akzeptierte Antwort sagt Ihnen tatsächlich, wie man X macht. Dann drehen sich die meisten anderen um und sagen, ich werde Ihnen nicht nur X sagen (was eindeutig möglich ist), sondern Sie sollten auch Y machen. Die meisten Unit-Test-Tools (ich nicht Ich spreche hier nur von JavaScript) und kann private Funktionen / Methoden testen. Ich werde weiter erklären, warum, weil es im JS-Land verloren gegangen zu sein scheint (anscheinend angesichts der Hälfte der Antworten).
Quaternion
13
Es ist eine gute Programmierpraxis, ein Problem in überschaubare Aufgaben zu unterteilen. Die Funktion "foo (x: Typ)" ruft daher die privaten Funktionen a (x: Typ), b (x: Typ), c (y: Ein anderer_Typ) und d ( z: Yet_another_type). Jetzt, da foo die Anrufe verwaltet und Dinge zusammenstellt, erzeugt es eine Art Turbulenz, wie die Rückseiten von Felsen in einem Bach, Schatten, die wirklich schwer zu gewährleisten sind, dass alle Bereiche getestet werden. Daher ist es einfacher sicherzustellen, dass jede Teilmenge von Bereichen gültig ist. Wenn Sie versuchen, das übergeordnete "foo" allein zu testen, wird die Bereichsprüfung in einigen Fällen sehr kompliziert.
Quaternion
18
Dies bedeutet nicht, dass Sie die öffentliche Schnittstelle nicht testen, offensichtlich auch, aber durch Testen der privaten Methoden können Sie eine Reihe von kurzen, verwaltbaren Blöcken testen (aus demselben Grund, aus dem Sie sie ursprünglich geschrieben haben, warum sollten Sie sie rückgängig machen? Dies, wenn es um das Testen geht) und nur weil die Tests auf öffentlichen Schnittstellen gültig sind (möglicherweise schränkt die aufrufende Funktion die Eingabebereiche ein), bedeutet dies nicht, dass die privaten Methoden nicht fehlerhaft sind, wenn Sie erweiterte Logik hinzufügen und sie von anderen aufrufen neue übergeordnete Funktionen,
Quaternion
5
Wenn Sie sie richtig mit TDD getestet haben, werden Sie nicht versuchen herauszufinden, was zum Teufel Sie später gemacht haben, wenn Sie sie richtig hätten testen sollen.
Quaternion

Antworten:

343

Ich bin bei Ihnen, obwohl es ein gutes Ziel ist, "nur die öffentliche API zu testen", gibt es Zeiten, in denen es nicht so einfach erscheint und Sie das Gefühl haben, zwischen einer Kompromittierung der API oder der Komponententests zu wählen. Sie wissen das bereits, da Sie genau darum bitten, also werde ich nicht darauf eingehen. :) :)

In TypeScript habe ich einige Möglichkeiten entdeckt, wie Sie zum Testen von Einheiten auf private Mitglieder zugreifen können. 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 die Teilnehmer , mit private, protected, public, hat die kompilierte JS keine privaten Mitglieder, da dies nicht eine Sache in JS ist. Es wird ausschließlich für den TS-Compiler verwendet. Dafür:

  1. Sie können anydem Compiler versichern, dass er Sie vor Zugriffsbeschränkungen warnt:

    (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 richtig machen any, sodass Sie nicht die 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

    Dies wird das Refactoring offensichtlich schwieriger machen.

  2. Sie können array access ( []) verwenden, um zu den privaten Mitgliedern zu gelangen:

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

    Während es funky aussieht, überprüft TSC die Typen tatsächlich so, als hätten Sie direkt darauf zugegriffen:

    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. Dies ist anscheinend eine absichtliche "Notluke" , um Ihnen Zugang zu privaten Mitgliedern zu verschaffen, ohne die Typensicherheit zu verlieren. Genau das möchten Sie meiner Meinung nach für Ihre Unit-Tests.

Hier ist ein Arbeitsbeispiel auf dem TypeScript-Spielplatz .

Bearbeiten für TypeScript 2.6

Eine andere Option, die manche mögen, ist die Verwendung // @ts-ignore( hinzugefügt in TS 2.6 ), die einfach alle Fehler in der folgenden Zeile unterdrückt:

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

Das Problem dabei ist, dass alle Fehler in der folgenden Zeile unterdrückt werden:

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

Ich persönlich betrachte @ts-ignoreeinen Code-Geruch und wie die Dokumente sagen:

Wir empfehlen Ihnen, diese Kommentare sehr sparsam zu verwenden . [Schwerpunkt Original]

Aaron Beall
quelle
45
Es ist so schön, eine realistische Haltung zu Unit-Tests zusammen mit einer tatsächlichen Lösung zu hören, anstatt das Standard-Dogma eines Unit-Testers.
d512
2
Einige "offizielle" Erklärungen des Verhaltens (in denen sogar Unit-Tests als Anwendungsfall genannt werden): github.com/microsoft/TypeScript/issues/19335
Aaron Beall
1
Verwenden Sie einfach "// @ ts-ignore", wie unten angegeben. um dem Linter zu sagen, er solle den privaten Accessor ignorieren
Tommaso
1
@Tommaso Ja, das ist eine weitere Option, hat aber den gleichen Nachteil as any: Sie verlieren alle Typprüfungen.
Aaron Beall
2
Beste Antwort, die ich seit einiger Zeit gesehen habe, danke @AaronBeall. Und danke auch tymspy für die ursprüngliche Frage.
nicolas.leblanc
26

Sie können private Methoden aufrufen . Wenn Sie den folgenden Fehler festgestellt haben:

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(/*...*/);
Mir-Ismaili
quelle
das sollte oben sein!
jsnewbie
2
Dies ist sicherlich eine weitere Option. Es hat das gleiche Problem wie das, as anydass Sie jede Typprüfung verlieren, tatsächlich verlieren Sie jede Typprüfung in der gesamten Zeile.
Aaron Beall
19

Da die meisten Entwickler nicht empfehlen, die private Funktion zu testen, sollten Sie sie testen.

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

Vielen Dank an @Aaron, @Thierry Templier.

tymspy
quelle
1
Ich denke, Typoskript gibt Flusenfehler, wenn Sie versuchen, eine private / geschützte Methode aufzurufen.
Gudgip
1
@Gudgip würde es Typfehler geben und wird nicht kompiliert. :)
tymspy
10

Schreiben Sie keine Tests für private Methoden. Dies macht den Punkt von Unit-Tests zunichte.

  • Sie sollten die öffentliche API Ihrer Klasse testen
  • Sie sollten NICHT die Implementierungsdetails Ihrer Klasse 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 sich später die Implementierung ändert, behaviourdie öffentliche API jedoch unverändert 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 vereinfachen.
Martin
quelle
3
Vielleicht lesen Sie den Beitrag, bevor Sie ihn kommentieren. Ich stelle klar und deutlich, dass das Testen von Privaten eher nach Testimplementierung als nach Verhalten riecht, was zu fragilen Tests führt.
Martin
1
Stellen Sie sich ein Objekt vor, das Ihnen eine Zufallszahl zwischen 0 und dem Privateigentum x gibt. Wenn Sie wissen möchten, ob x vom Konstruktor korrekt festgelegt wurde, ist es viel einfacher, den Wert von x zu testen, als hundert Tests durchzuführen, um zu überprüfen, ob die erhaltenen Zahlen im richtigen Bereich liegen.
Galdor
1
@ user3725805 Dies ist ein Beispiel für das Testen der Implementierung, nicht des Verhaltens. Es wäre besser zu isolieren, woher die private Nummer kommt: eine Konstante, eine Konfiguration, ein Konstruktor - und von dort aus zu testen. Wenn das Private nicht aus einer anderen Quelle stammt, fällt es in das Antipattern "magische Zahl".
Martin
1
Und warum darf die Implementierung nicht getestet werden? Unit-Tests sind gut, um unerwartete Änderungen zu erkennen. Wenn der Konstruktor aus irgendeinem Grund vergisst, die Nummer festzulegen, schlägt der Test sofort fehl und warnt mich. Wenn jemand die Implementierung ändert, schlägt der Test ebenfalls fehl, aber ich bevorzuge es, einen Test zu übernehmen, als einen unerkannten Fehler zu haben.
Galdor
2
+1. Gute Antwort. @ TimJames Die richtige Praxis zu erklären oder auf den fehlerhaften Ansatz hinzuweisen, ist der eigentliche Zweck von SO. Anstatt einen hackigen Weg zu finden, um das zu erreichen, was das OP will.
Syed Aqeel Ashiq
4

Der Punkt "Private Methoden nicht testen" ist wirklich, die Klasse wie jemand zu testen , der sie verwendet .

Wenn Sie eine öffentliche API mit 5 Methoden haben, kann jeder Verbraucher Ihrer Klasse diese verwenden. Daher sollten Sie sie testen. Ein Verbraucher sollte nicht auf die privaten Methoden / Eigenschaften Ihrer Klasse zugreifen. Dies bedeutet, dass Sie private Mitglieder ändern können, wenn die öffentlich zugängliche Funktionalität gleich bleibt.


Wenn Sie sich auf interne erweiterbare Funktionen verlassen, verwenden Sie protectedstattdessen private.
Beachten Sie, dass dies protectedimmer noch eine öffentliche API (!) Ist , die nur 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;
    }
}

Unit-Test-geschützte Eigenschaften auf die gleiche Weise, wie ein Verbraucher sie verwenden würde, über Unterklassen:

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

Das hat bei mir funktioniert:

Anstatt:

sut.myPrivateMethod();

Dies:

sut['myPrivateMethod']();
Gehirnhändler
quelle
2

Entschuldigen Sie den Nekro in diesem Beitrag, aber ich fühle mich gezwungen, ein paar Dinge abzuwägen, die anscheinend nicht berührt wurden.

In erster Linie - wenn wir während des Unit-Tests Zugang zu privaten Mitgliedern einer Klasse benötigen, ist dies im Allgemeinen eine große, fette rote Fahne, die wir in unserem strategischen oder taktischen Ansatz vermasselt haben und versehentlich durch Druck auf das Prinzip der Einzelverantwortung verstoßen haben Verhalten, wo es nicht hingehört. Das Gefühl, auf Methoden zugreifen zu müssen, die eigentlich nichts anderes als eine isolierte Unterroutine eines Konstruktionsverfahrens sind, ist eines der häufigsten Ereignisse. Es ist jedoch so, als würde Ihr Chef erwarten, dass Sie bereit zur Arbeit erscheinen und auch pervers wissen müssen, welche Morgenroutine Sie durchlaufen haben, um in diesen Zustand zu gelangen ...

Das andere häufigste Beispiel dafür ist, dass Sie versuchen, die sprichwörtliche "Gottklasse" zu testen. Es ist an und für sich eine besondere Art von Problem, leidet jedoch unter dem gleichen Grundproblem, wenn man intime Details eines Verfahrens kennen muss - aber das kommt vom Thema ab.

In diesem speziellen Beispiel haben wir die Verantwortung für die vollständige Initialisierung des Bar-Objekts effektiv dem Konstruktor der FooBar-Klasse zugewiesen. Bei der objektorientierten Programmierung ist einer der Hauptgründe, dass der Konstruktor "heilig" ist und vor ungültigen Daten geschützt werden sollte, die seinen eigenen internen Zustand ungültig machen und ihn darauf vorbereiten würden, irgendwo anders stromabwärts zu versagen (in einer möglicherweise sehr tiefen Situation) Pipeline.)

Wir haben dies hier nicht getan, indem wir dem FooBar-Objekt erlaubt haben, einen Balken zu akzeptieren, der zum Zeitpunkt der Erstellung der FooBar noch nicht bereit ist, und dies durch eine Art "Hacking" des FooBar-Objekts kompensiert haben, um die Dinge selbst in die 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 Status eines Objekts vollständig initialisiert und bereit sein sollte, eingehende Anrufe an seine öffentlichen Mitglieder unmittelbar nach der Erstellung zu verarbeiten. Dies bedeutet nicht, dass der Konstruktor in allen Fällen unmittelbar aufgerufen wird. Wenn Sie ein Objekt mit vielen komplexen Konstruktionsszenarien haben, ist es besser, Setter seinen optionalen Elementen einem Objekt auszusetzen, das gemäß einem Erstellungsentwurfsmuster (Factory, Builder usw.) in einem der folgenden Elemente implementiert ist die letzteren Fälle,

In Ihrem Beispiel scheint sich die Eigenschaft "status" der Leiste nicht in einem gültigen Zustand zu befinden, in dem eine FooBar sie akzeptieren kann. Die FooBar unternimmt also etwas, um dieses Problem zu beheben.

Das zweite Problem, das ich sehe, ist, dass Sie anscheinend versuchen, Ihren Code zu testen, anstatt eine testgetriebene Entwicklung zu üben. Dies ist definitiv meine eigene Meinung zu diesem Zeitpunkt; Aber diese Art des Testens ist wirklich ein Anti-Muster. Am Ende geraten Sie in die Falle, zu erkennen, dass Sie Kernprobleme beim Design haben, die verhindern, dass Ihr Code nachträglich testbar ist, anstatt die benötigten Tests zu schreiben und anschließend auf die Tests zu programmieren. In jedem Fall sollten Sie immer noch die gleiche Anzahl von Tests und Codezeilen haben, wenn Sie wirklich eine SOLID-Implementierung erreicht haben. Warum also versuchen, Ihren Weg in testbaren Code zurückzuentwickeln, wenn Sie die Angelegenheit zu Beginn Ihrer Entwicklungsbemühungen nur ansprechen können?

Wenn Sie das getan hätten, hätten Sie viel früher erkannt, dass Sie einen ziemlich ekligen Code schreiben müssten, um gegen Ihr Design zu testen, und hätten frühzeitig die Möglichkeit gehabt, Ihren Ansatz neu auszurichten, indem Sie das Verhalten auf Implementierungen verlagerten, die dies tun sind leicht testbar.

Ryan Hansen
quelle
2

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

Wenn Sie Ihre private Methode jedoch wirklich testen möchten, können Sie sich darüber im Klaren sein, dass der entsprechende Code für das TypeScript einer Methode des Prototyps der Konstruktorfunktion entspricht. Dies bedeutet, dass es zur Laufzeit verwendet werden kann (während Sie wahrscheinlich einige Kompilierungsfehler haben werden).

Beispielsweise:

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 transpiliert 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 Plunkr: https://plnkr.co/edit/calJCF?p=preview .

Thierry Templier
quelle
1

Wie viele bereits gesagt haben, sollten Sie Ihren Code oder Transpiler nicht hacken, damit er für Sie funktioniert, so oft Sie die privaten Methoden testen möchten. Das heutige TypeScript wird fast alle Hacks ablehnen, die die Leute bisher bereitgestellt haben.


Lösung

TLDR ; Wenn eine Methode getestet werden soll, sollten Sie den Code in eine Klasse entkoppeln, mit der Sie die zu testende Methode öffentlich machen können.

Der Grund, warum Sie die Methode privat haben, ist, dass die Funktionalität nicht unbedingt von dieser Klasse verfügbar gemacht werden muss. Wenn die Funktionalität nicht dorthin gehört, sollte sie daher in ihre eigene Klasse entkoppelt werden.

Beispiel

Ich bin auf diesen Artikel gestoßen, der hervorragend erklärt, wie Sie das Testen privater Methoden angehen sollten. Es werden sogar einige der Methoden hier behandelt und wie sie schlechte Implementierungen sind.

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

Hinweis : Dieser Code wird aus dem oben verlinkten Blog entfernt (ich dupliziere, falls sich der Inhalt hinter dem Link ändert).

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

    private getUserAddress(){
        //...
        this.formatStreet();
        //...
    }
    private formatStreet(){
        //...
    }
}
Nach dem
class User{
    private address:Address;
    public getUserInformationToDisplay(){
        //...
        address.getUserAddress();
        //...
    }
}
class Address{
    private format: StreetFormatter;
    public format(){
        //...
        format.ToString();
        //...
    }
}
class StreetFormatter{
    public toString(){
        // ...
    }
}
CTS_AE
quelle
1

Rufen Sie die private Methode in eckigen Klammern auf

Ts-Datei

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

spect.ts Datei

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

Die Antwort von Aaron ist die beste und funktioniert für mich :) Ich würde darüber abstimmen, aber leider kann ich 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 haben.

Beispielsweise:

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 auf einmal zu testen, da wir diese privaten Methoden verspotten müssten, die wir nicht verspotten können, weil wir nicht auf sie zugreifen können. Dies bedeutet, dass wir viel Konfiguration für einen Komponententest benötigen, um dies als Ganzes zu testen.

Der beste Weg, um die oben beschriebene Methode mit allen Abhängigkeiten zu testen, ist ein End-to-End-Test, da hier ein Integrationstest erforderlich ist. Der E2E-Test hilft Ihnen jedoch nicht, wenn Sie TDD (Test Driven Development) üben, sondern testen jede Methode wird.

Devpool
quelle
0

Auf diesem Weg 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
}

Jetzt weiß ich nicht, gegen welche Art von OOP-Regeln ich verstoße, aber um die Frage zu beantworten, teste ich auf diese Weise private Methoden. Ich begrüße jeden, der diesbezüglich Vor- und Nachteile berät.

Sani Yusuf
quelle