web-dev-qa-db-fra.com

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

Comment testez-vous 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 l'étendue externe. 

    Plus tard, supprimez le code de test à l'aide d'un outil . http://philipwalton.com/articles/how-to-unit-test-private-functions-in-javascript/

S'il vous plaît, suggérez-moi un meilleur moyen de résoudre ce problème si vous en avez fait?

P.S 

  1. La plupart des réponses à des questions similaires à 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 ont tort ou raison, mais il est nécessaire que mon cas teste les fonctions privées.

98
tymspy

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

Dans TypeScript, j'ai découvert plusieurs façons d'accéder aux 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 TS restreint l'accès aux membres de la classe à l'aide de private, protected, public, le JS compilé n'a aucun membre privé, car ce n'est pas une chose dans JS. C'est purement utilisé pour le compilateur TS. Pour cela:

  1. Vous pouvez affirmer à any et éviter au 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 simplement aucune idée de ce que vous faites à droite de la variable any. Par conséquent, 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
    
  2. Vous pouvez utiliser l'accès au tableau ([]) pour obtenir les membres privés:

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

    Bien que cela semble génial, TSC validera 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 semble que les crochets de tableau n'appliquent pas de restrictions d'accès, mais l'inférence de type vous donne une sécurité de type totale. C'est exactement ce que je pense que vous voulez pour vos tests unitaires.

Voici un exemple de travail dans le TypeScript Playground .

186
Aaron

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.

16
tymspy

N'écrivez pas de tests pour les méthodes privées. Cela défait le point des tests unitaires. 

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

Exemple

class SomeClass {

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

Le test de cette méthode ne doit pas nécessairement changer si l'implémentation change ultérieurement, mais que la variable behaviour de l'API 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 communiquez pas les méthodes et les propriétés uniquement pour les tester. Cela signifie généralement que:

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

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(/*...*/);
4
Mir-Ismaili

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

Tout d’abord, quand nous avons besoin d’avoir accès à des membres privés lors d’un cours, c’est généralement un grand 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 où il n'appartient pas. L'une des occurrences les plus courantes consiste à ressentir le besoin d'accéder à des méthodes qui ne sont en réalité qu'un sous-programme isolé d'une procédure de construction. Cependant, c'est un peu comme si votre patron s'attendait à ce que vous vous présentiez au travail et que vous ayez un besoin pervers de savoir quelle routine matinale vous avez suivie pour vous mettre dans cet état ...

L’autre exemple le plus courant de cet événement se produit lorsque vous essayez de tester la proverbiale "classe de dieu". Il s'agit d'un type de problème particulier en soi, mais le même problème fondamental réside dans le fait qu'il est nécessaire 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é de l'initialisation complète de l'objet Bar au constructeur de la classe FooBar. Dans la programmation orientée objet, l’un des principes fondamentaux est que le constructeur est "sacré" et doit être protégé contre les données non valides qui invalideraient son propre état interne et le laisseraient en échec ailleurs en aval (ce qui pourrait être très profond). pipeline.) 

Nous n’avons pas réussi à faire cela ici en permettant à l’objet FooBar d’accepter une barre qui n’est pas prête au moment de la construction du FooBar, et avons compensé par une sorte de "piratage" de l’objet FooBar pour prendre les choses en main. mains. 

Ceci est le résultat d'un échec d'adhésion à un autre contenu de la programmation orientée objet (dans le cas de Bar,), à savoir que l'état d'un objet doit être entièrement initialisé et prêt à gérer tous les appels entrants destinés à ses membres publics immédiatement après leur création. Maintenant, cela ne signifie pas immédiatement après l'appel du constructeur dans toutes les instances. Lorsque vous avez un objet comportant de nombreux scénarios de construction complexes, il est préférable d'exposer les régleurs à ses membres facultatifs à un objet implémenté conformément à un modèle de conception (usine, constructeur, etc.). Dans ce dernier cas, vous pousseriez l’initialisation de l’objet cible vers un autre graphe d’objets dont le seul objectif est d’orienter le trafic vers un point où vous avez une instance valide de ce que vous demandez - et le produit ne doit pas être considéré comme "prêt" jusqu'à ce que cet objet de création l'ait servi. 

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 - le FooBar y fait donc 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 définitivement ma propre opinion à ce stade; mais, ce type de test est vraiment un anti-modèle. Vous finissez par tomber dans le piège de réaliser 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 les programmer par la suite. Quoi qu’il en soit, vous devriez quand même vous retrouver avec le même nombre de tests et le même nombre de lignes de code si vous aviez vraiment réalisé une implémentation SOLID. Alors, pourquoi essayer de faire de l’ingénierie inverse du code testable alors que vous pouvez simplement régler le problème dès le début de vos efforts de développement? 

Si vous aviez fait cela, alors vous auriez compris beaucoup plus tôt que vous deviez écrire un code plutôt épineux afin de tester votre conception, et vous auriez eu l’occasion de réaligner votre approche en déplaçant le comportement vers des implémentations qui sont facilement testables. 

2
Ryan Hansen

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

Si vous avez une API publique avec 5 méthodes, tout 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 utilisez des fonctionnalités internes extensibles, utilisez protected au lieu de private.
Notez que protected est toujours 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;
    }
}

Testez les propriétés protégées de la même manière qu'un consommateur les utiliserait, via un 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]);
});
2
Leon Adler

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

Mais si vous voulez vraiment tester votre méthode privée, sachez que le code correspondant à TypeScript correspond à une méthode du prototype de fonction constructeur. Cela signifie qu'il peut être utilisé au moment de 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 ceci plunkr: https://plnkr.co/edit/calJCF?p=preview .

1
Thierry Templier

Cet itinéraire que je prends est celui 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 OOP règles que je respecte, mais pour répondre à la question, voici comment je teste les méthodes privées. Je souhaite la bienvenue à quiconque pour conseiller sur les avantages et les inconvénients de cela. 

0
Sani Yusuf

La réponse de 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 la seule façon de les utiliser et d'avoir un code vierge 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 en même temps, car nous aurions besoin de simuler ces méthodes privées, ce que nous ne pouvons pas, car nous ne pouvons pas y accéder. Cela signifie que nous avons besoin de beaucoup de configuration pour un test unitaire afin de le tester dans son ensemble.

Ceci dit, la meilleure façon 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 le développement piloté par les tests (TDD), mais n'importe quelle méthode le fera. 

0
Devpool

En option, vous pouvez utiliser protégé à la place privé et déclarer le "test" enfant dans votre test.

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

}

dans les 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');
  });
...
0
Menshchikov Aleksandr