Comment écrire des tests unitaires pour Angular / TypeScript pour des méthodes privées avec Jasmine


199

Comment tester une fonction privée dans 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 solution que j'ai trouvée

  1. Placez le code de test lui-même dans la fermeture ou ajoutez du code dans la fermeture qui stocke les références aux variables locales sur les objets existants dans la portée externe.

    Supprimez ultérieurement le code de test à l'aide d'un outil. http://philipwalton.com/articles/how-to-unit-test-private-functions-in-javascript/

Veuillez me suggérer une meilleure façon de résoudre ce problème si vous en avez fait?

PS

  1. La plupart des réponses à un type de question similaire comme celle-ci ne donnent pas de solution au problème, c'est pourquoi je pose cette question

  2. La plupart des développeurs disent que vous ne testez pas les fonctions privées, mais je ne dis pas qu'elles sont fausses ou correctes, mais il est nécessaire que mon cas teste privé.


11
Les tests doivent uniquement tester l'interface publique, pas l'implémentation privée. Les tests que vous effectuez sur l'interface publique doivent également couvrir la partie privée.
toskv

16
J'aime le fait que la moitié des réponses devraient en fait être des commentaires. OP pose une question, comment allez-vous X? La réponse acceptée vous indique en fait comment faire X. Ensuite, la plupart des autres se retournent et disent, non seulement je ne vous dirai pas X (ce qui est clairement possible) mais vous devriez faire Y. La plupart des outils de test unitaire (je ne le suis pas) parlant uniquement de JavaScript ici) sont capables de tester des fonctions / méthodes privées. Je vais continuer à expliquer pourquoi parce qu'il semble s'être perdu dans JS land (apparemment, étant donné la moitié des réponses).
Quaternion

13
C'est une bonne pratique de programmation de décomposer un problème en tâches gérables, donc la fonction "foo (x: type)" appellera les fonctions privées a (x: type), b (x: type), c (y: another_type) et d ( z: encore_un autre_type). Maintenant parce que foo gère les appels et rassemble les choses, cela crée une sorte de turbulence, comme l'arrière des rochers dans un ruisseau, des ombres qui sont vraiment difficiles à vérifier que toutes les plages sont testées. En tant que tel, il est plus facile de s'assurer que chaque sous-ensemble de plages est valide, si vous essayez de tester le parent "toto" seul, le test de plage devient très compliqué dans certains cas.
Quaternion

18
Cela ne veut pas dire que vous ne testez pas l'interface publique, évidemment vous le faites, mais tester les méthodes privées vous permet de tester une série de petits morceaux gérables (la même raison pour laquelle vous les avez écrits en premier lieu, pourquoi voudriez-vous annuler ceci en ce qui concerne les tests), et ce n'est pas parce que les tests sur les interfaces publiques sont valides (peut-être que la fonction appelante restreint les plages d'entrée) que les méthodes privées ne sont pas défectueuses lorsque vous ajoutez une logique plus avancée et que vous les appelez depuis d'autres nouvelles fonctions parentales,
Quaternion

5
si vous les avez testés correctement avec TDD, vous n'essaierez pas de comprendre ce que vous faisiez plus tard, alors que vous auriez dû les tester correctement.
Quaternion

Réponses:


349

Je suis avec vous, même si c'est un bon objectif de "tester uniquement l'API publique", il y a des moments où cela ne semble pas aussi simple et vous sentez que vous choisissez entre compromettre l'API ou les tests unitaires. Vous le savez déjà, puisque c'est exactement ce que vous demandez de faire, donc je ne vais pas y entrer. :)

Dans TypeScript, j'ai découvert plusieurs façons d'accéder à des membres privés à des fins de test unitaire. Considérez cette 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; }

}

Même si l'accès des TS aux membres de la classe en utilisant private, protected, publicle JS compilé n'a pas de membres privés, puisque ce n'est pas une chose à JS. Il est uniquement utilisé pour le compilateur TS. À cet effet:

  1. Vous pouvez affirmer anyet empêcher le compilateur de vous avertir des restrictions d'accès:

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

    Le problème avec cette approche est que le compilateur n'a tout simplement aucune idée de ce que vous faites à droite du any, donc vous n'obtenez pas les erreurs de type souhaitées:

    (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

    Cela rendra évidemment le refactoring plus difficile.

  2. Vous pouvez utiliser array access ( []) pour accéder aux membres privés:

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

    Bien que cela ait l'air génial, TSC validera en fait les types comme si vous y aviez accédé directement:

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

    Pour être honnête, je ne sais pas pourquoi cela fonctionne. Il s'agit apparemment d'une "trappe d'évacuation" intentionnelle pour vous donner accès aux membres privés sans perdre la sécurité du type. C'est exactement ce que je pense que vous voulez pour vos tests unitaires.

Voici un exemple de travail dans le TypeScript Playground .

Modifier pour TypeScript 2.6

Une autre option que certains aiment est d'utiliser // @ts-ignore( ajoutée dans TS 2.6 ) qui supprime simplement toutes les erreurs sur la ligne suivante:

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

Le problème avec ceci est que cela supprime toutes les erreurs sur la ligne suivante:

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

Personnellement, je considère @ts-ignoreune odeur de code, et comme le disent les documents:

nous vous recommandons d'utiliser ces commentaires avec parcimonie . [italique dans l'original]


46
Tellement agréable d'entendre une position réaliste sur les tests unitaires avec une solution réelle plutôt que votre dogme de testeur d'unité standard.
d512

2
Quelques explications "officielles" du comportement (qui cite même les tests unitaires comme cas d'utilisation): github.com/microsoft/TypeScript/issues/19335
Aaron Beall

1
Utilisez simplement` // @ ts-ignore` comme indiqué ci-dessous. dire au linter d'ignorer l'accesseur privé
Tommaso

1
@Tommaso Ouais, c'est une autre option, mais a le même inconvénient d'utiliser as any: vous perdez toute vérification de type.
Aaron Beall

2
Meilleure réponse que j'ai vue depuis un moment, merci @AaronBeall. Et aussi, merci Tymspy d'avoir posé la question initiale.
nicolas.leblanc

27

Vous pouvez appeler des méthodes privées . Si vous avez rencontré l'erreur suivante:

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

utilisez simplement // @ts-ignore:

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

cela devrait être au top!
jsnewbie

2
C'est certainement une autre option. Il souffre du même problème que as anydans la mesure où vous perdez toute vérification de type, en fait vous perdez toute vérification de type sur toute la ligne.
Aaron Beall le

20

Comme la plupart des développeurs ne recommandent pas de tester la fonction privée , pourquoi ne pas la tester?.

Par exemple.

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

Merci à @Aaron, @Thierry Templier.


1
Je pense que dactylographié donne des erreurs de peluchage lorsque vous essayez d'appeler une méthode privée / protégée.
Gudgip

1
@Gudgip cela donnerait des erreurs de type et ne compilerait pas. :)
tymspy

10

N'écrivez pas de tests pour les méthodes privées. Cela va à l'encontre de l'intérêt des tests unitaires.

  • Vous devriez tester l'API publique de votre classe
  • Vous ne devriez PAS tester les détails d'implimentation de votre classe

Exemple

class SomeClass {

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

Le test de cette méthode ne devrait pas avoir besoin de changer si ultérieurement l'implémentation change mais que l' behaviourAPI publique reste la même.

class SomeClass {

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

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

Ne rendez pas les méthodes et les propriétés publiques uniquement pour les tester. Cela signifie généralement que:

  1. Vous essayez de tester l'implémentation plutôt que l'API (interface publique).
  2. Vous devez déplacer la logique en question dans sa propre classe pour faciliter les tests.

3
Peut-être lire le message avant de le commenter. Je déclare et démontre clairement que les tests privés sont une odeur de mise en œuvre de tests plutôt que de comportement, ce qui conduit à des tests fragiles.
Martin

1
Imaginez un objet qui vous donne un nombre aléatoire entre 0 et la propriété privée x. Si vous voulez savoir si x est correctement défini par le constructeur, il est beaucoup plus facile de tester la valeur de x que de faire une centaine de tests pour vérifier si les nombres que vous obtenez sont dans la bonne plage.
Galdor

1
@ user3725805 ceci est un exemple de test de l'implémentation, pas du comportement. Il vaudrait mieux isoler d'où vient le numéro privé: une constante, une configuration, un constructeur - et tester à partir de là. Si le privé ne provient pas d'une autre source, alors il tombe dans l'anti-modèle "nombre magique".
Martin

1
Et pourquoi n'est-il pas autorisé à tester l'implémentation? Les tests unitaires sont utiles pour détecter les changements inattendus. Lorsque, pour une raison quelconque, le constructeur oublie de définir le nombre, le test échoue immédiatement et me prévient. Quand quelqu'un change l'implémentation, le test échoue aussi, mais je préfère adopter un test plutôt que d'avoir une erreur non détectée.
Galdor

2
+1. Très bonne réponse. @TimJames Dire la bonne pratique ou souligner l'approche défectueuse est le but même de SO. Au lieu de trouver un moyen extrêmement fragile de réaliser tout ce que le PO veut.
Syed Aqeel Ashiq

4

Le but de "ne pas tester les méthodes privées" est vraiment de tester la classe comme quelqu'un qui l'utilise .

Si vous avez une API publique avec 5 méthodes, n'importe quel consommateur de votre classe peut les utiliser et vous devez donc les tester. Un consommateur ne doit pas accéder aux méthodes / propriétés privées de votre classe, ce qui signifie que vous pouvez modifier les membres privés lorsque la fonctionnalité exposée publique reste la même.


Si vous comptez sur une fonctionnalité extensible interne, utilisez à la protectedplace de private.
Notez qu'il protecteds'agit toujours d'une API publique (!) , Juste utilisée différemment.

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

Test unitaire des propriétés protégées de la même manière qu'un consommateur les utiliserait, via le sous-classement:

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

Cela a fonctionné pour moi:

Au lieu de:

sut.myPrivateMethod();

Ce:

sut['myPrivateMethod']();

2

Désolé pour le nécro sur ce post, mais je me sens obligé de peser sur deux choses qui ne semblent pas avoir été abordées.

Tout d'abord - lorsque nous avons besoin d'accéder à des membres privés sur une classe pendant les tests unitaires, c'est généralement un gros drapeau rouge que nous avons gaffé dans notre approche stratégique ou tactique et avons violé par inadvertance le principe de responsabilité unique en poussant comportement là où il n’appartient pas. Ressentir le besoin d'accéder à des méthodes qui ne sont en réalité rien de plus qu'un sous-programme isolé d'une procédure de construction est l'une des occurrences les plus courantes de ceci; cependant, c'est un peu comme si votre patron s'attend à ce que vous vous présentiez au travail prêt à partir et qu'il ait aussi un besoin pervers de savoir quelle routine matinale vous avez traversée pour vous mettre dans cet état ...

L'autre exemple le plus courant de ce phénomène est celui où vous vous trouvez en train d'essayer de tester la proverbiale «classe de Dieu». C'est un type particulier de problème en soi, mais il souffre du même problème de base avec le besoin de connaître les détails intimes d'une procédure - mais cela sort du sujet.

Dans cet exemple spécifique, nous avons effectivement attribué la responsabilité d'initialiser complètement l'objet Bar au constructeur de la classe FooBar. Dans la programmation orientée objet, l'un des principaux tenants est que le constructeur est "sacré" et doit être protégé contre les données invalides qui invalideraient son propre état interne et le laisseraient prêt à échouer ailleurs en aval (dans ce qui pourrait être un pipeline.)

Nous n'avons pas réussi à le faire ici en permettant à l'objet FooBar d'accepter une barre qui n'est pas prête au moment où la FooBar est construite, et avons compensé en "piratant" l'objet FooBar pour prendre les choses en soi mains.

Ceci est le résultat d'un échec d'adhérer à un autre tenent de la programmation orientée objet (dans le cas de Bar,) qui est que l'état d'un objet doit être entièrement initialisé et prêt à gérer tous les appels entrants à ses `` membres publics '' immédiatement après sa création. Maintenant, cela ne signifie pas immédiatement après l'appel du constructeur dans toutes les instances. Lorsque vous avez un objet qui a de nombreux scénarios de construction complexes, il est préférable d'exposer les setters à ses membres facultatifs à un objet qui est implémenté conformément à un modèle de conception de création (Factory, Builder, etc ...) dans l'un des ces derniers cas,

Dans votre exemple, la propriété "status" de la barre ne semble pas être dans un état valide dans lequel un FooBar peut l'accepter - donc le FooBar fait quelque chose pour corriger ce problème.

Le deuxième problème que je vois est qu'il semble que vous essayez de tester votre code plutôt que de pratiquer le développement piloté par les tests. C'est certainement ma propre opinion en ce moment; mais, ce type de test est vraiment un anti-pattern. Ce que vous finissez par faire, c'est tomber dans le piège de vous rendre compte que vous avez des problèmes de conception fondamentaux qui empêchent votre code d'être testable après coup, plutôt que d'écrire les tests dont vous avez besoin et de programmer ensuite les tests. Quoi qu'il en soit, vous rencontrez le problème, vous devriez toujours vous retrouver avec le même nombre de tests et de lignes de code si vous aviez vraiment réalisé une implémentation SOLID. Alors, pourquoi essayer de faire de l'ingénierie inverse dans un code testable alors que vous pouvez simplement résoudre le problème au début de vos efforts de développement?

Si vous aviez fait cela, alors vous auriez réalisé bien plus tôt que vous alliez devoir écrire du code plutôt dégoûtant afin de le tester par rapport à votre conception et que vous auriez eu l'opportunité dès le début de réaligner votre approche en déplaçant le comportement vers des implémentations qui sont facilement testables.


2

Je suis d'accord avec @toskv: je ne recommanderais pas de faire ça :-)

Mais si vous voulez vraiment tester votre méthode privée, vous pouvez être conscient que le code correspondant pour le TypeScript correspond à une méthode du prototype de la fonction constructeur. Cela signifie qu'il peut être utilisé à l'exécution (alors que vous aurez probablement des erreurs de compilation).

Par exemple:

export class FooBar {
  private _status: number;

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

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

sera transpilé en:

(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);

Voir ce plunkr: https://plnkr.co/edit/calJCF?p=preview .


1

Comme beaucoup l'ont déjà dit, même si vous souhaitez tester les méthodes privées, vous ne devez pas pirater votre code ou votre transpilateur pour le faire fonctionner pour vous. TypeScript moderne refusera la plupart des hacks que les gens ont fournis jusqu'à présent.


Solution

TLDR ; si une méthode doit être testée, vous devez découpler le code en une classe que vous pouvez exposer à la méthode publique pour être testée.

La raison pour laquelle vous avez la méthode privée est que la fonctionnalité n'appartient pas nécessairement à être exposée par cette classe, et donc si la fonctionnalité n'y appartient pas, elle doit être découplée dans sa propre classe.

Exemple

J'ai parcouru cet article qui explique très bien comment vous devez vous attaquer aux tests de méthodes privées. Il couvre même certaines des méthodes ici et explique pourquoi elles sont de mauvaises implémentations.

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

Remarque : ce code est extrait du blog lié ci-dessus (je duplique au cas où le contenu derrière le lien changerait)

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

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

1

appeler une méthode privée en utilisant des crochets

Fichier TS

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

fichier 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 réponse d' Aaron est la meilleure et fonctionne pour moi :) Je voterais mais malheureusement je ne peux pas (réputation manquante).

Je dois dire que tester des méthodes privées est le seul moyen de les utiliser et d'avoir un code propre de l'autre côté.

Par exemple:

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

Il est très logique de ne pas tester toutes ces méthodes à la fois, car nous aurions besoin de nous moquer de ces méthodes privées, ce que nous ne pouvons pas simuler parce que nous ne pouvons pas y accéder. Cela signifie que nous avons besoin de beaucoup de configuration pour un test unitaire pour tester cela dans son ensemble.

Ceci dit, le meilleur moyen de tester la méthode ci-dessus avec toutes les dépendances est un test de bout en bout, car ici un test d'intégration est nécessaire, mais le test E2E ne vous aidera pas si vous pratiquez TDD (Test Driven Development), mais testez toute méthode le fera.


0

Cette route que je prends est celle où je crée des fonctions en dehors de la classe et assigne la fonction à ma méthode privée.

export class MyClass {
  private _myPrivateFunction = someFunctionThatCanBeTested;
}

function someFunctionThatCanBeTested() {
  //This Is Testable
}

Maintenant, je ne sais pas quel type de règles OOP je suis en train de briser, mais pour répondre à la question, voici comment je teste les méthodes privées. Je souhaite à quiconque de vous conseiller sur les avantages et les inconvénients de cela.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.