Pregunta Cómo escribir pruebas unitarias para Angular 2 / TypeScript para métodos privados con Jasmine


¿Cómo se prueba una función privada en 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 solución que encontré

  1. Coloque el código de prueba dentro del cierre o agregue código dentro del cierre que almacena referencias a las variables locales en objetos existentes en el ámbito externo.

    Más tarde quita el código de prueba con una herramienta. http://philipwalton.com/articles/how-to-unit-test-private-functions-in-javascript/

Por favor sugiérame una mejor manera de resolver este problema si lo has hecho alguna vez?

PD

  1. La mayor parte de la respuesta para un tipo similar de pregunta como esta no da una solución al problema, es por eso que estoy haciendo esta pregunta

  2. La mayoría de los desarrolladores dicen que no pruebes las funciones privadas, pero no digo que estén equivocadas o sean correctas, pero mi caso necesita pruebas privadas.


74
2018-03-14 12:03


origen


Respuestas:


Estoy contigo, aunque es un buen objetivo "probar solo la API pública", hay veces en que no parece tan simple y sientes que estás eligiendo entre comprometer la API o las pruebas unitarias. Ya lo sabes, ya que eso es exactamente lo que estás pidiendo que hagas, así que no voy a entrar en eso. :)

En TypeScript descubrí algunas maneras en que puede acceder a miembros privados por el bien de las pruebas unitarias. Considera esta clase:

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

}

Aunque TS restringe el acceso a los miembros de la clase usando private, protected, public, el JS compilado no tiene miembros privados, ya que esto no es algo en JS. Es puramente utilizado para el compilador TS. Por lo tanto:

  1. Puedes afirmar a any y evitar que el compilador le advierta sobre las restricciones de acceso:

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

    El problema con este enfoque es que el compilador simplemente no tiene idea de lo que está haciendo bien de la any, por lo que no obtendrá los errores de tipo deseados:

    (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. Puede usar el acceso a la matriz ([]) para llegar a los miembros privados:

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

    Si bien parece funky, TSC realmente validará los tipos como si los hubiera accedido directamente:

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

    Para ser sincero, no sé por qué funciona esto. Parece que los corchetes de la matriz no imponen restricciones de acceso, pero la inferencia tipo te da seguridad total. Esto es exactamente lo que creo que quiere para su unidad de prueba.

Aquí hay un ejemplo de trabajo en el Playground de TypeScript.


118
2018-03-14 15:24



Como la mayoría de los desarrolladores no recomiendo probar la función privada, ¿Por qué no probarlo ?.

P.ej.

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

Gracias a @Aaron, @Thierry Templier.


16
2018-03-18 06:54



No escriba pruebas para métodos privados. Esto derrota el punto de pruebas unitarias.

  • Deberías probar la API pública de tu clase
  • NO deberías estar probando los detalles de implementación de tu clase

Ejemplo

class SomeClass {

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

La prueba de este método no debería cambiar si más adelante cambia la implementación, pero la behaviour de la API pública sigue siendo el mismo.

class SomeClass {

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

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

No haga que los métodos y las propiedades sean públicos solo para probarlos. Esto generalmente significa que:

  1. Está intentando probar la implementación en lugar de API (interfaz pública).
  2. Debería mover la lógica en cuestión a su propia clase para facilitar las pruebas.

11
2018-03-14 13:30



El punto de "no probar métodos privados" realmente es Pon a prueba la clase como alguien que lo usa.

Si tiene una API pública con 5 métodos, cualquier consumidor de su clase puede usarla y, por lo tanto, debe probarla. Un consumidor no debe acceder a los métodos / propiedades privados de su clase, lo que significa que puede cambiar miembros privados cuando la funcionalidad pública expuesta permanece igual.


Si confía en la funcionalidad extensible interna, use protected en lugar de private.
Tenga en cuenta que protected sigue siendo un API pública (!), solo usado de manera diferente.

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

Pruebe las propiedades protegidas de la prueba de la misma manera que un consumidor las usaría, mediante subclases:

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
2017-07-01 09:28



Perdón por el necro en esta publicación, pero me siento obligado a opinar sobre un par de cosas que no parecen haber sido tocadas.

Primero que nada: cuando nos encontramos necesitando acceso a miembros privados en una clase durante las pruebas unitarias, generalmente es una bandera roja grande y gorda que hemos utilizado en nuestro enfoque estratégico o táctico y hemos violado inadvertidamente el principio de responsabilidad única presionando comportamiento donde no pertenece. Sentir la necesidad de acceder a métodos que en realidad no son más que una subrutina aislada de un procedimiento de construcción es una de las ocurrencias más comunes de esto; sin embargo, es algo así como que su jefe espera que se presente para el trabajo listo para ir y que también tenga una necesidad perversa de saber qué rutina matutina usted pasó para ingresarlo a ese estado ...

La otra instancia más común de esto que sucede es cuando te encuentras tratando de probar la proverbial "clase de Dios". Es un tipo especial de problema en sí mismo, pero adolece del mismo problema básico de la necesidad de conocer detalles íntimos de un procedimiento, pero eso se está alejando del tema.

En este ejemplo específico, hemos asignado efectivamente la responsabilidad de inicializar completamente el objeto Bar al constructor de la clase FooBar. En la programación orientada a objetos, uno de los aspectos fundamentales es que el constructor es "sagrado" y debe protegerse contra datos no válidos que invalidarían su propio estado interno y lo dejarían preparado para fallar en otro lugar aguas abajo (en lo que podría ser una profundidad tubería.)

No hemos podido hacer eso al permitir que el objeto FooBar acepte una barra que no está lista en el momento en que se construye el FooBar, y se han compensado mediante una especie de "pirateo" del objeto FooBar para llevar las cosas a su propia cuenta manos.

Esto es el resultado de una falla al adherirse a otra tenencia de programación orientada a objetos (en el caso de Bar,) que es que el estado de un objeto debe inicializarse completamente y estar listo para manejar cualquier llamada entrante a sus miembros públicos inmediatamente después de la creación. Ahora, esto no significa inmediatamente después de llamar al constructor en todas las instancias. Cuando tiene un objeto que tiene muchos escenarios de construcción complejos, entonces es mejor exponer a los instaladores a sus miembros opcionales a un objeto que se implementa de acuerdo con un patrón de diseño de creación (Fábrica, Constructor, etc.) en cualquiera de en los últimos casos, estaría empujando la inicialización del objeto objetivo hacia otro gráfico de objetos cuyo único propósito es dirigir el tráfico para llevarlo a un punto donde tenga una instancia válida de la que está solicitando, y el producto no debería ser considerado "listo" hasta después de que este objeto de creación lo haya servido.

En su ejemplo, la propiedad de "estado" del Bar no parece estar en un estado válido en el que un FooBar pueda aceptarlo, por lo que el FooBar le hace algo para corregir ese problema.

El segundo problema que estoy viendo es que parece que estás tratando de probar tu código en lugar de practicar el desarrollo basado en pruebas. Esta es definitivamente mi propia opinión en este momento; pero, este tipo de prueba es realmente un antipatrón. Lo que termina haciendo es caer en la trampa de darse cuenta de que tiene problemas de diseño central que impiden que su código sea comprobable después del hecho, en lugar de escribir las pruebas que necesita y posteriormente programar las pruebas. De cualquier forma que veas el problema, aún así deberías tener la misma cantidad de pruebas y líneas de código si realmente hubieras logrado una implementación SÓLIDA. Entonces, ¿por qué intentar e invertir la ingeniería inversa en un código comprobable cuando puede abordar el asunto al inicio de sus esfuerzos de desarrollo?

Si lo hubieras hecho, te habrías dado cuenta mucho antes de que tendrías que escribir un código bastante repulsivo para probar en contra de tu diseño y habrías tenido la oportunidad desde el principio de realinear tu enfoque cambiando el comportamiento a implementaciones que son fácilmente comprobables.


2
2017-12-22 06:08



Estoy de acuerdo con @toskv: NO RECOMENDARÍA HACER ESO:-)

Pero si realmente desea probar su método privado, puede tener en cuenta que el código correspondiente para el TypeScript corresponde a un método del prototipo de función del constructor. Esto significa que puede usarse en tiempo de ejecución (mientras que es probable que tenga algunos errores de compilación).

Por ejemplo:

export class FooBar {
  private _status: number;

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

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

será transpilado 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);

Ver este plunkr: https://plnkr.co/edit/calJCF?p=preview.


1
2018-03-14 13:19



Como muchos ya han indicado, por mucho que quiera probar los métodos privados, no debe hackear su código o transpiler para que funcione para usted. El día moderno TypeScript negará la mayoría de los ataques que las personas han proporcionado hasta ahora.


Solución

TLDR; si un método debe ser probado, entonces debes desacoplar el código en una clase que pueda exponer el método para que sea público para ser probado.

La razón por la que tiene el método privado es porque la funcionalidad no necesariamente debe ser expuesta por esa clase, y por lo tanto, si la funcionalidad no pertenece allí, debe estar desacoplada en su propia clase.

Ejemplo

Me encontré con este artículo que hace un gran trabajo al explicar cómo debe abordar las pruebas de métodos privados. Incluso cubre algunos de los métodos aquí y cómo son malas implementaciones.

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

Nota: Este código se elimina del blog vinculado anteriormente (estoy duplicando en caso de que el contenido detrás del enlace cambie)

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

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

0
2018-06-26 22:48