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

196

¿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 almacene referencias a las variables locales en objetos existentes en el ámbito externo.

    Luego, elimine el código de prueba con una herramienta. http://philipwalton.com/articles/how-to-unit-test-private-functions-in-javascript/

¿Me ha sugerido una mejor manera de resolver este problema si ya lo hizo?

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 hago esta pregunta

  2. La mayoría de los desarrolladores dicen que no pruebes las funciones privadas, pero no digo que sean incorrectas o correctas, pero es necesario que mi caso sea privado.

tymspy
fuente
11
las pruebas solo deberían probar la interfaz pública, no la implementación privada. Las pruebas que realice en la interfaz pública también deberían cubrir la parte privada.
toskv
16
Me gusta cómo la mitad de las respuestas deberían ser comentarios. OP hace una pregunta, ¿cómo X? La respuesta aceptada en realidad te dice cómo hacer X. Luego, la mayoría del resto se da vuelta y dice, no solo no te diré X (que es claramente posible) sino que deberías estar haciendo Y. La mayoría de las herramientas de prueba de unidades (no estoy hablando solo de JavaScript aquí) son capaces de probar funciones / métodos privados. Continuaré explicando por qué porque parece haberse perdido en tierra de JS (aparentemente, dadas la mitad de las respuestas).
Quaternion
13
Es una buena práctica de programación dividir un problema en tareas manejables, por lo que la función "foo (x: type)" llamará a funciones privadas a (x: type), b (x: type), c (y: another_type) y d ( z: yet_another_type). Ahora, debido a que está gestionando las llamadas y uniendo las cosas, crea una especie de turbulencia, como los lados posteriores de las rocas en una corriente, sombras que son realmente difíciles de garantizar que se prueben todos los rangos. Como tal, es más fácil asegurarse de que cada subconjunto de rangos sea válido, si intenta probar el "foo" padre solo, la prueba de rango se vuelve muy complicada en los casos.
Quaternion
18
Esto no quiere decir que no pruebe la interfaz pública, obviamente lo hace, pero probar los métodos privados le permite probar una serie de fragmentos cortos y manejables (la misma razón por la que los escribió en primer lugar, ¿por qué deshacería? esto cuando se trata de pruebas), y solo porque las pruebas en las interfaces públicas son válidas (tal vez la función de llamada restringe los rangos de entrada) no significa que los métodos privados no tengan fallas cuando agrega lógica más avanzada y los llama desde otro nuevas funciones para padres,
Quaternion
55
si los probó correctamente con TDD, no intentará averiguar qué demonios estaba haciendo más tarde, cuando debería haberlos probado correctamente.
Quaternion

Respuestas:

343

Estoy con usted, a pesar de que es un buen objetivo "probar solo la unidad de la API pública", hay momentos en que no parece tan simple y siente que está 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 he descubierto algunas formas en que puede acceder a miembros privados por el simple hecho de realizar 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; }

}

A pesar de que el acceso TS restringe a miembros de la clase usando private, protected, public, el JS compilado no tiene usuarios privados, ya que esto no es una cosa en JS. Se usa exclusivamente para el compilador de TS. Por lo tanto:

  1. Puede hacer valer anyy evitar que el compilador le advierta sobre 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 correctamente any, por lo que no obtiene 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

    Obviamente, esto hará que la refactorización sea más difícil.

  2. Puede usar el acceso a la matriz ( []) para acceder 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 accediera a ellos 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. Aparentemente, esta es una "escotilla de escape" intencional para darle acceso a miembros privados sin perder la seguridad del tipo. Esto es exactamente lo que creo que quieres para tu prueba de unidad.

Aquí hay un ejemplo de trabajo en TypeScript Playground .

Editar para TypeScript 2.6

Otra opción que a algunos les gusta es usar // @ts-ignore( agregado en TS 2.6 ) que simplemente suprime todos los errores en la siguiente línea:

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

El problema con esto es, bueno, suprime todos los errores en la siguiente línea:

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

Personalmente considero @ts-ignoreun olor a código, y como dicen los documentos:

Le recomendamos que use estos comentarios con moderación . [énfasis original]

Aaron Beall
fuente
45
Es muy agradable escuchar una postura realista sobre las pruebas unitarias junto con una solución real en lugar del dogma del probador de unidades estándar.
d512
2
Alguna explicación "oficial" del comportamiento (que incluso cita pruebas unitarias como un caso de uso): github.com/microsoft/TypeScript/issues/19335
Aaron Beall
1
Simplemente use `// @ ts-ignore` como se señala a continuación. decirle a la peluquera que ignore el descriptor de acceso privado
Tommaso
1
@Tommaso Sí, esa es otra opción, pero tiene la misma desventaja de usar as any: pierde toda la verificación de tipos.
Aaron Beall
2
La mejor respuesta que he visto en mucho tiempo, gracias @AaronBeall. Y también, gracias tymspy por hacer la pregunta original.
nicolas.leblanc
26

Puedes llamar a métodos privados . Si encontró el siguiente error:

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

solo usa // @ts-ignore:

// @ts-ignore
expect(new FooBar(/*...*/).initFooBar()).toEqual(/*...*/);
Mir-Ismaili
fuente
Esto debería estar en la parte superior!
jsnewbie
2
Esta es ciertamente otra opción. Sufre el mismo problema, ya as anyque pierde cualquier verificación de tipo, en realidad pierde cualquier verificación de tipo en toda la línea.
Aaron Beall
19

Como la mayoría de los desarrolladores no recomiendan probar la función privada , ¿por qué no probarla?

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.

tymspy
fuente
1
Creo que el mecanografiado da errores de linting cuando intentas llamar a un método privado / protegido.
Gudgip
1
@Gudgip daría errores de tipo y no se compilará. :)
tymspy
10

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

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

Ejemplo

class SomeClass {

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

La prueba para este método no debería necesitar cambiar si luego la implementación cambia, pero la behaviourAPI pública sigue siendo la misma.

class SomeClass {

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

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

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

  1. Está intentando probar la implementación en lugar de la API (interfaz pública).
  2. Debe mover la lógica en cuestión a su propia clase para facilitar las pruebas.
Martín
fuente
3
Tal vez lea la publicación antes de comentarla. Afirmo y demuestro claramente que las pruebas privadas son un olor a prueba de implementación en lugar de comportamiento, lo que lleva a pruebas frágiles.
Martin
1
Imagina un objeto que te da un número aleatorio entre 0 y la propiedad privada x. Si desea saber si el constructor establece correctamente x, es mucho más fácil probar el valor de x que hacer cientos de pruebas para verificar si los números que obtiene están en el rango correcto.
Galdor
1
@ user3725805 este es un ejemplo de prueba de la implementación, no del comportamiento. Sería mejor aislar de dónde proviene el número privado: una constante, una configuración, un constructor, y probar desde allí. Si lo privado no proviene de otra fuente, entonces cae en el antipatrón "número mágico".
Martin
1
¿Y por qué no está permitido probar la implementación? Las pruebas unitarias son buenas para detectar cambios inesperados. Cuando por alguna razón el constructor olvida establecer el número, la prueba falla inmediatamente y me advierte. Cuando alguien cambia la implementación, la prueba también falla, pero prefiero adoptar una prueba que tener un error no detectado.
Galdor
2
+1. Gran respuesta. @TimJames Decir la práctica correcta o señalar el enfoque defectuoso es el propósito mismo de SO. En lugar de encontrar una forma hacky-frágil de lograr lo que quiera el OP.
Syed Aqeel Ashiq
4

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

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


Si confía en la funcionalidad interna extensible, use en protectedlugar de private.
Tenga en cuenta que protectedtodavía es una API pública (!) , Solo que se usa 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;
    }
}

Prueba unitaria de propiedades protegidas de la misma manera que un consumidor las usaría, a través de 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]);
});
Leon Adler
fuente
3

Esto funcionó para mí:

En vez de:

sut.myPrivateMethod();

Esta:

sut['myPrivateMethod']();
ferviente
fuente
2

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, lo más importante: cuando nos encontramos necesitando acceso a miembros privados en una clase durante las pruebas de la unidad, generalmente es una gran y gruesa bandera roja que nos hemos burlado de nuestro enfoque estratégico o táctico y hemos violado inadvertidamente el principio de responsabilidad individual al presionar comportamiento donde no pertenece. Sentir la necesidad de acceder a métodos que realmente no son más que una subrutina aislada de un procedimiento de construcción es uno de los casos más comunes de esto; sin embargo, es como si tu jefe esperara que aparecieras listo para trabajar y también tuviera una necesidad perversa de saber qué rutina matutina seguiste para llegar a ese estado ...

La otra instancia más común de este suceso es cuando te encuentras tratando de probar la proverbial "clase de dios". Es un tipo especial de problema en sí mismo, pero sufre el mismo problema básico con la necesidad de conocer detalles íntimos de un procedimiento, pero eso está saliendo 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 principales núcleos 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 muy profundo). tubería.)

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

Este es el resultado de una falla en adherirse a otra unidad de programación orientada a objetos (en el caso de Bar), que es que el estado de un objeto debe estar completamente inicializado y 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 que se llame al constructor en todas las instancias. Cuando tiene un objeto que tiene muchos escenarios de construcción complejos, es mejor exponer a los setters a sus miembros opcionales a un objeto que se implemente de acuerdo con un patrón de diseño de creación (Factory, Builder, etc.) en cualquiera de los últimos casos

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

El segundo problema que veo es que parece que está intentando probar su 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 centrales de diseño que impiden que su código sea verificable después del hecho, en lugar de escribir las pruebas que necesita y posteriormente programarlas para las pruebas. De cualquier forma que se encuentre con el problema, aún debería terminar con la misma cantidad de pruebas y líneas de código si realmente hubiera logrado una implementación SÓLIDA. Entonces, ¿por qué tratar de aplicar ingeniería inversa en código comprobable cuando puede abordar el asunto al comienzo de sus esfuerzos de desarrollo?

Si lo hubiera hecho, entonces se habría dado cuenta mucho antes de que tendría que escribir un código bastante asqueroso para probar su diseño y habría tenido la oportunidad desde el principio de realinear su enfoque cambiando el comportamiento a implementaciones que son fácilmente comprobables

Ryan Hansen
fuente
2

Estoy de acuerdo con @toskv: no recomendaría hacer eso :-)

Pero si realmente quiere probar su método privado, puede tener en cuenta que el código correspondiente para TypeScript corresponde a un método del prototipo de la función de constructor. Esto significa que puede usarse en tiempo de ejecución (mientras que probablemente tendrá 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á trasladado a:

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

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

Thierry Templier
fuente
1

Como muchos ya han dicho, por mucho que quieras probar los métodos privados, no debes hackear tu código o transpiler para que funcione para ti. TypeScript moderno negará la mayoría de los hacks que la gente ha proporcionado hasta ahora.


Solución

TLDR ; si un método debe ser probado, entonces debe desacoplar el código en una clase en la 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 estar expuesta por esa clase y, por lo tanto, si la funcionalidad no pertenece allí, debe desacoplarse en su propia clase.

Ejemplo

Me encontré con este artículo que hace un gran trabajo al explicar cómo debes abordar probar 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 cambie el contenido detrás del enlace)

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(){
        // ...
    }
}
CTS_AE
fuente
1

llamar al método privado usando corchetes

Archivo Ts

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

archivo spect.ts

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

La respuesta de Aaron es la mejor y está funcionando para mí :) Lo votaría pero lamentablemente no puedo (falta reputación).

Debo decir que probar métodos privados es la única forma de usarlos y tener un código limpio en el otro lado.

Por ejemplo:

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

Tiene mucho sentido no probar todos estos métodos a la vez porque tendríamos que burlarnos de esos métodos privados, que no podemos burlarnos porque no podemos acceder a ellos. Esto significa que necesitamos mucha configuración para que una prueba unitaria pruebe esto en su conjunto.

Esto dijo que la mejor manera de probar el método anterior con todas las dependencias es una prueba de extremo a extremo, porque aquí se necesita una prueba de integración, pero la prueba E2E no lo ayudará si está practicando TDD (Test Driven Development), pero prueba Cualquier método lo hará.

Devpool
fuente
0

Esta ruta que tomo es una en la que creo funciones fuera de la clase y asigno la función a mi método privado.

export class MyClass {
  private _myPrivateFunction = someFunctionThatCanBeTested;
}

function someFunctionThatCanBeTested() {
  //This Is Testable
}

Ahora no sé qué tipo de reglas de OOP estoy rompiendo, pero para responder a la pregunta, así es como pruebo los métodos privados. Doy la bienvenida a cualquiera para que asesore sobre Pros y Contras de esto.

Sani Yusuf
fuente