Como escrever testes de unidade para Angular / TypeScript para métodos particulares com Jasmine


196

Como você testa uma função privada no 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;
    }

}

A solução que encontrei

  1. Coloque o próprio código de teste dentro do fechamento ou Adicionar código dentro do fechamento que armazena referências às variáveis ​​locais em objetos existentes no escopo externo.

    Posteriormente, retire o código de teste usando uma ferramenta. http://philipwalton.com/articles/how-to-unit-test-private-functions-in-javascript/

Por favor, sugira-me uma maneira melhor de resolver este problema, se você tiver feito algum?

PS

  1. A maior parte da resposta para um tipo semelhante de pergunta como esta não fornece uma solução para o problema, é por isso que estou fazendo esta pergunta

  2. A maioria dos desenvolvedores diz que você não testa funções privadas, mas eu não digo que elas estão erradas ou corretas, mas há necessidade de meu caso testar particular.


11
Os testes devem testar apenas a interface pública, não a implementação privada. Os testes que você faz na interface pública também devem abranger a parte privada.
toskv

16
Eu gosto de como metade das respostas devem ser realmente comentários. OP pergunta: como você X? A resposta aceita, na verdade, diz como você faz o X. Depois, a maioria do resto se vira e diz: não apenas eu não vou lhe dizer o X (o que é claramente possível), mas você deve fazer o Y. A maioria das ferramentas de teste de unidade (eu não sou falando apenas de JavaScript aqui) são capazes de testar funções / métodos privados. Vou continuar explicando o porquê, porque parece ter se perdido na terra do JS (aparentemente, dadas metade das respostas).
Quaternion

13
É uma boa prática de programação dividir um problema em tarefas gerenciáveis, portanto, a função "foo (x: type)" chamará as funções privadas a (x: tipo), b (x: tipo), c (y: outro_tipo) ed ( z: yet_another_type). Agora, porque foo, gerenciando as chamadas e organizando as coisas, cria uma espécie de turbulência, como as laterais das rochas em um riacho, sombras que são realmente difíceis de garantir que todos os intervalos sejam testados. Como tal, é mais fácil garantir que cada subconjunto de intervalos seja válido, se você tentar testar o pai "foo" sozinho, o teste de intervalo se tornará muito complicado nos casos.
Quaternion

18
Isso não quer dizer que você não teste a interface pública, obviamente, mas testar os métodos privados permite testar uma série de pequenos blocos gerenciáveis ​​(o mesmo motivo pelo qual você os escreveu em primeiro lugar, por que você iria desfazer isso quando se trata de teste) e apenas porque os testes nas interfaces públicas são válidos (talvez a função de chamada restrinja os intervalos de entrada) não significa que os métodos privados não sejam defeituosos quando você adiciona lógica mais avançada e os chama de outros novas funções dos pais,
Quaternion

5
se você os testou corretamente com TDD, não tentará descobrir o que diabos estava fazendo depois, quando deveria tê-los testado corretamente.
Quaternion

Respostas:


343

Estou com você, mesmo que seja um bom objetivo "testar unicamente a API pública", há momentos em que isso não parece tão simples e você sente que está escolhendo entre comprometer a API ou os testes de unidade. Você já sabe disso, já que é exatamente isso que você está pedindo para fazer, então não vou entrar nisso. :)

No TypeScript, descobri algumas maneiras de acessar membros privados para fins de teste de unidade. Considere esta 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; }

}

Mesmo que TS restringe o acesso a membros da classe usando private, protected, public, a JS compilado não tem membros privados, uma vez que esta não é uma coisa em JS. É puramente usado para o compilador TS. Para isso:

  1. Você pode afirmar anye evitar que o compilador o avise sobre restrições de acesso:

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

    O problema dessa abordagem é que o compilador simplesmente não faz ideia do que você está fazendo certo any, para que você não obtenha os erros de tipo desejados:

    (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, isso tornará a refatoração mais difícil.

  2. Você pode usar o array access ( []) para acessar os membros privados:

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

    Embora pareça estranho, o TSC validará os tipos como se você os tivesse acessado diretamente:

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

    Para ser sincero, não sei por que isso funciona. Aparentemente, essa é uma "escotilha de escape" intencional para fornecer acesso a membros privados sem perder a segurança do tipo. É exatamente o que eu acho que você deseja para o seu teste de unidade.

Aqui está um exemplo de trabalho no TypeScript Playground .

Editar para TypeScript 2.6

Outra opção que alguns gostam é usar // @ts-ignore( adicionado no TS 2.6 ) que simplesmente suprime todos os erros na seguinte linha:

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

O problema disso é que, bem, suprime todos os erros na seguinte linha:

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

Pessoalmente, considero @ts-ignoreum cheiro de código e, como dizem os documentos:

recomendamos que você use esses comentários com moderação . [ênfase original]


45
É tão bom ouvir uma postura realista sobre o teste de unidade, juntamente com uma solução real, em vez do dogma padrão do testador de unidade.
D512 7/04

2
Alguma explicação "oficial" do comportamento (que até cita o teste de unidade como um caso de uso): github.com/microsoft/TypeScript/issues/19335
Aaron Beall

1
Basta usar `// @ ts-ignore` como indicado abaixo. para dizer a linter ignorar o acessador particular
Tommaso

1
@ Tommaso Sim, essa é outra opção, mas tem a mesma desvantagem de usar as any: você perde toda a verificação de tipo.
Aaron Beall

2
Melhor resposta que já vi há algum tempo, obrigado @AaronBeall. E também, obrigado tymspy por fazer a pergunta original.
Nicolas.leblanc 29/10/19

26

Você pode chamar métodos privados . Se você encontrou o seguinte erro:

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

basta usar // @ts-ignore:

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

isso deve estar no topo!
Jsnewbie #

2
Esta é certamente outra opção. Ele sofre do mesmo problema as anyque você perde qualquer verificação de tipo, na verdade você perde qualquer verificação de tipo em toda a linha.
Aaron Beall

19

Como a maioria dos desenvolvedores não recomenda testar a função privada , por que não testá-la?

Por exemplo.

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

Graças a @Aaron, @Thierry Templier.


1
Eu acho que o texto datilografado gera erros de fiapos quando você tenta chamar um método privado / protegido.
Gudgip

1
@Gudgip daria erros de tipo e não compila. :)
tymspy

10

Não escreva testes para métodos particulares. Isso anula o ponto dos testes de unidade.

  • Você deve testar a API pública da sua classe
  • Você NÃO deve testar os detalhes de implementação da sua turma

Exemplo

class SomeClass {

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

O teste desse método não precisará ser alterado se posteriormente a implementação for alterada, mas a behaviourAPI pública permanecer a mesma.

class SomeClass {

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

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

Não torne públicos métodos e propriedades apenas para testá-los. Isso geralmente significa que:

  1. Você está tentando testar a implementação em vez da API (interface pública).
  2. Você deve mover a lógica em questão para sua própria classe para facilitar o teste.

3
Talvez leia o post antes de comentar. Afirmo e demonstro claramente que testar privates é um cheiro de implementação de testes, e não de comportamento, o que leva a testes frágeis.
Martin

1
Imagine um objeto que lhe dê um número aleatório entre 0 e a propriedade privada x. Se você deseja saber se x é definido corretamente pelo construtor, é muito mais fácil testar o valor de x do que fazer uma centena de testes para verificar se os números obtidos estão no intervalo correto.
Galdor

1
@ user3725805 este é um exemplo de teste da implementação, não do comportamento. Seria melhor isolar de onde vem o número privado: uma constante, uma configuração, construtor - e testar a partir daí. Se o privado não vier de outra fonte, ele se enquadra no antipadrão "número mágico".
Martin

1
E por que não é permitido testar a implementação? Os testes de unidade são bons para detectar alterações inesperadas. Quando, por algum motivo, o construtor se esquece de definir o número, o teste falha imediatamente e me avisa. Quando alguém altera a implementação, o teste também falha, mas eu prefiro adotar um teste do que ter um erro não detectado.
Galdor

2
+1. Ótima resposta. @ TimJames Contar a prática correta ou apontar a abordagem falha é o próprio objetivo do SO. Em vez de encontrar uma maneira frágil de hackers de conseguir o que o OP deseja.
Syed Aqeel Ashiq

4

O objetivo de "não testar métodos particulares" é realmente Testar a classe como alguém que a usa .

Se você possui uma API pública com 5 métodos, qualquer consumidor de sua classe pode usá-los e, portanto, deve testá-los. Um consumidor não deve acessar os métodos / propriedades particulares de sua classe, o que significa que você pode alterar membros privados quando a funcionalidade exposta ao público permanecer a mesma.


Se você confiar na funcionalidade extensível interna, use em protectedvez de private.
Observe que protectedainda é uma API pública (!) , Usada apenas de forma 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;
    }
}

O teste de unidade protegeu as propriedades da mesma maneira que um consumidor as usaria, via subclassificação:

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

Isso funcionou para mim:

Ao invés de:

sut.myPrivateMethod();

Este:

sut['myPrivateMethod']();

2

Desculpem o necro deste post, mas sinto-me compelido a considerar algumas coisas que parecem não ter sido tocadas.

Em primeiro lugar - quando nos encontramos precisando de acesso a membros particulares de uma classe durante o teste de unidade, geralmente é uma bandeira vermelha grande e ridícula que brincamos em nossa abordagem estratégica ou tática e violamos inadvertidamente o diretor de responsabilidade única pressionando comportamento onde não pertence. Sentir a necessidade de acessar métodos que nada mais são do que uma sub-rotina isolada de um procedimento de construção é uma das ocorrências mais comuns; no entanto, é como se seu chefe esperasse que você aparecesse para o trabalho pronto para ir e também tenha alguma necessidade perversa de saber por que rotina matinal você passou para entrar nesse estado ...

O outro exemplo mais comum desse acontecimento é quando você tenta testar a proverbial "classe divina". É um tipo especial de problema por si só, mas sofre do mesmo problema básico por precisar conhecer detalhes íntimos de um procedimento - mas isso está saindo do tópico.

Neste exemplo específico, atribuímos efetivamente a responsabilidade de inicializar totalmente o objeto Bar ao construtor da classe FooBar. Na programação orientada a objetos, um dos principais inquilinos é que o construtor é "sagrado" e deve ser protegido contra dados inválidos que invalidariam seu próprio estado interno e o deixariam preparado para falhar em outro lugar a jusante (no que poderia ser muito profundo). pipeline.)

Falhamos em fazer isso aqui, permitindo que o objeto FooBar aceite uma barra que não está pronta no momento em que o FooBar é construído e compensamos com uma espécie de "invasão" do objeto FooBar para resolver os problemas por conta própria mãos

Isso é o resultado de uma falha em aderir a outro conteúdo da programação orientada a objetos (no caso de Bar), que significa que o estado de um objeto deve estar totalmente inicializado e pronto para lidar com as chamadas recebidas de seus membros públicos imediatamente após a criação. Agora, isso não significa imediatamente após o construtor ser chamado em todas as instâncias. Quando você tem um objeto que possui muitos cenários de construção complexos, é melhor expor os setters a seus membros opcionais a um objeto implementado de acordo com um padrão de design de criação (Factory, Builder, etc ...) os últimos casos,

No seu exemplo, a propriedade "status" da barra não parece estar em um estado válido no qual um FooBar possa aceitá-la - portanto, a FooBar faz algo para corrigir esse problema.

O segundo problema que estou vendo é que parece que você está tentando testar seu código em vez de praticar o desenvolvimento orientado a testes. Esta é definitivamente a minha opinião neste momento; mas, esse tipo de teste é realmente um anti-padrão. O que você acaba fazendo é cair na armadilha de perceber que possui problemas essenciais de design que impedem que seu código seja testável após o fato, em vez de gravar os testes necessários e, posteriormente, programar os testes. De qualquer forma, você enfrenta o problema, ainda deve ter o mesmo número de testes e linhas de código se realmente tiver conseguido uma implementação do SOLID. Então - por que tentar fazer uma engenharia reversa em código testável quando você pode apenas resolver o problema no início de seus esforços de desenvolvimento?

Se você tivesse feito isso, teria percebido muito antes que teria que escrever um código bastante nojento para testar seu design e teria tido a oportunidade desde o início de realinhar sua abordagem, mudando o comportamento para implementações que são facilmente testáveis.


2

Concordo com @toskv: eu não recomendaria fazer isso :-)

Mas se você realmente deseja testar seu método privado, pode estar ciente de que o código correspondente para o TypeScript corresponde a um método do protótipo da função do construtor. Isso significa que ele pode ser usado em tempo de execução (enquanto você provavelmente terá alguns erros de compilação).

Por exemplo:

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 para:

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

Veja este artigo: https://plnkr.co/edit/calJCF?p=preview .


1

Como muitos já declararam, por mais que você queira testar os métodos particulares, não deve hackear seu código ou transpiler para fazê-lo funcionar. Atualmente, o TypeScript negará a maioria dos hacks que as pessoas forneceram até agora.


Solução

TLDR ; se um método deve ser testado, você deve dissociar o código em uma classe que possa expor o método a ser público a ser testado.

O motivo de você ter o método privado é que a funcionalidade não pertence necessariamente a ser exposta por essa classe e, portanto, se a funcionalidade não pertencer a ela, ela deve ser dissociada em sua própria classe.

Exemplo

Encontrei este artigo que explica muito bem como você deve lidar com o teste de métodos particulares. Ele ainda cobre alguns dos métodos aqui e como eles são implementações ruins.

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

Nota : este código foi retirado do blog vinculado acima (estou duplicando caso o conteúdo por trás do link seja alterado)

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

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

1

chamar método privado usando colchetes

Arquivo Ts

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

arquivo spect.ts

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

0

A resposta de Aaron é a melhor e está funcionando para mim :) Eu votaria, mas infelizmente não posso (falta de reputação).

Devo dizer que testar métodos particulares é a única maneira de usá-los e ter um código limpo do outro lado.

Por exemplo:

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

Faz muito sentido não testar todos esses métodos de uma só vez, porque precisaríamos zombar desses métodos privados, que não podemos zombar porque não podemos acessá-los. Isso significa que precisamos de muita configuração para um teste de unidade para testar isso como um todo.

Dito isso, a melhor maneira de testar o método acima com todas as dependências é um teste completo, porque aqui é necessário um teste de integração, mas o teste E2E não ajudará se você estiver praticando TDD (Test Driven Development), mas testando qualquer método será.


0

Essa rota que eu tomo é aquela em que crio funções fora da classe e atribuo a função ao meu método privado.

export class MyClass {
  private _myPrivateFunction = someFunctionThatCanBeTested;
}

function someFunctionThatCanBeTested() {
  //This Is Testable
}

Agora não sei que tipo de regras de OOP estou violando, mas, para responder à pergunta, é assim que testo métodos privados. Congratulo-me com alguém para aconselhar sobre os prós e contras disso.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.