Jak napisać testy jednostkowe dla Angular / TypeScript dla prywatnych metod z Jasmine

196

Jak testujesz funkcję prywatną w 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;
    }

}

Rozwiązanie, które znalazłem

  1. Umieść sam kod testowy wewnątrz zamknięcia lub Dodaj kod wewnątrz zamknięcia, który przechowuje odwołania do zmiennych lokalnych na istniejących obiektach w zewnętrznym zakresie.

    Następnie usuń kod testowy za pomocą narzędzia. http://philipwalton.com/articles/how-to-unit-test-private-functions-in-javascript/

Proszę zasugerować mi lepszy sposób rozwiązania tego problemu, jeśli już to zrobiłeś?

PS

  1. Większość odpowiedzi na podobne pytania, takie jak to, nie daje rozwiązania problemu, dlatego zadaję to pytanie

  2. Większość programistów twierdzi, że nie testujesz funkcji prywatnych, ale nie twierdzę, że są one złe lub słuszne, ale mój przypadek wymaga przetestowania funkcji prywatnych.

tymspy
źródło
11
testy powinny testować tylko interfejs publiczny, a nie implementację prywatną. Testy przeprowadzane na interfejsie publicznym powinny obejmować również część prywatną.
toskv
16
Podoba mi się, jak połowa odpowiedzi powinna stanowić komentarz. OP zadaje pytanie, jak się masz X? Zaakceptowana odpowiedź faktycznie mówi ci, jak zrobić X. Następnie większość reszty się odwraca i mówi: nie tylko nie powiem ci X (co jest wyraźnie możliwe), ale powinieneś robić Y. Większość narzędzi do testowania jednostek (nie jestem mówiąc tylko o JavaScript) są w stanie przetestować prywatne funkcje / metody. Wyjaśnię dlaczego, ponieważ wydaje się, że zgubił się w ziemi JS (najwyraźniej, biorąc pod uwagę połowę odpowiedzi).
Quaternion
13
Dobrą praktyką programistyczną jest podział problemu na możliwe do zarządzania zadania, więc funkcja „foo (x: typ)” wywoła funkcje prywatne a (x: typ), b (x: typ), c (y: inny_typ) id ( z: yet_another_type). Teraz, ponieważ foo, zarządza połączeniami i zestawia rzeczy, tworzy rodzaj turbulencji, takich jak tylne boki skał w strumieniu, cienie, które są naprawdę trudne do zapewnienia, że ​​wszystkie zakresy są testowane. W związku z tym łatwiej jest upewnić się, że każdy podzestaw zakresów jest prawidłowy, jeśli spróbujesz samodzielnie przetestować nadrzędny „foo”, testowanie zakresów staje się w niektórych przypadkach bardzo skomplikowane.
Quaternion
18
Nie oznacza to, że nie testujesz interfejsu publicznego, oczywiście, że tak, ale testowanie prywatnych metod pozwala przetestować serię krótkich porcji do zarządzania (ten sam powód, dla którego je napisałeś, dlaczego miałbyś cofnąć dotyczy to testowania), a to, że testy publicznych interfejsów są prawidłowe (być może funkcja wywołująca ogranicza zakresy wejściowe), nie oznacza, że ​​metody prywatne nie są wadliwe, gdy dodaje się bardziej zaawansowaną logikę i wywołuje je z innych nowe funkcje nadrzędne,
Quaternion
5
jeśli przetestowałeś je poprawnie za pomocą TDD, nie będziesz próbował dowiedzieć się, co do cholery robiłeś później, kiedy powinieneś je poprawnie przetestować.
Quaternion

Odpowiedzi:

343

Jestem z tobą, mimo że dobrym celem jest „testowanie tylko publicznego interfejsu API”, zdarza się, że nie wydaje się to takie proste i masz wrażenie, że wybierasz między zagrożeniem dla interfejsu API lub testów jednostkowych. Wiesz już o tym, ponieważ dokładnie o to prosisz, więc nie będę w to wchodził. :)

W TypeScript odkryłem kilka sposobów uzyskiwania dostępu do prywatnych członków na potrzeby testów jednostkowych. Rozważ tę klasę:

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

}

Choć TS ogranicza dostęp do członków klasy za pomocą private, protected, public, skompilowany JS nie ma prywatnych członków, ponieważ nie jest to sprawa w JS. Jest używany wyłącznie w kompilatorze TS. W związku z tym:

  1. Możesz zapewnić anyi uniknąć kompilatora przed ostrzeżeniem o ograniczeniach dostępu:

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

    Problem z tym podejściem polega na tym, że kompilator po prostu nie ma pojęcia, co robisz od razu any, więc nie otrzymujesz pożądanych błędów typu:

    (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

    To oczywiście utrudni refaktoryzację.

  2. Możesz użyć tablicy access ( []), aby uzyskać dostęp do prywatnych członków:

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

    Choć wygląda na funky, TSC tak naprawdę sprawdza typy tak, jakbyś miał do nich bezpośredni dostęp:

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

    Szczerze mówiąc, nie wiem, dlaczego to działa. Jest to najwyraźniej celowa „klapa ratunkowa” zapewniająca dostęp do członków prywatnych bez utraty bezpieczeństwa typu. Właśnie tego oczekuję od testów jednostkowych.

Oto działający przykład na placu zabaw TypeScript .

Edycja dla TypeScript 2.6

Inną opcją, którą niektórzy lubią, jest // @ts-ignore( dodana w TS 2.6 ), która po prostu pomija wszystkie błędy w następującym wierszu:

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

Problem polega na tym, że pomija wszystkie błędy w następującym wierszu:

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

Osobiście uważam @ts-ignorezapach kodu i, jak mówią doktorzy:

zalecamy używanie tych komentarzy bardzo oszczędnie . [podkreślenie oryginalne]

Aaron Beall
źródło
45
Miło jest słyszeć realistyczne podejście do testów jednostkowych wraz z rzeczywistym rozwiązaniem, a nie standardowe dogmaty testerów jednostkowych.
d512,
2
Kilka „oficjalnych” wyjaśnień zachowania (które nawet przytacza testy jednostkowe jako przypadek użycia): github.com/microsoft/TypeScript/issues/19335
Aaron Beall
1
Wystarczy użyć ʻ // @ ts-ignore`, jak wskazano poniżej. powiedzieć liniowiecowi, aby zignorował prywatnego akcesora
Tommaso
1
@Tommaso Tak, to kolejna opcja, ale ma tę samą wadę użycia as any: tracisz wszystkie sprawdzanie typu.
Aaron Beall,
2
Najlepsza odpowiedź, jaką widziałem od dłuższego czasu, dzięki @AaronBeall. A także dzięki tymspy za zadanie oryginalnego pytania.
nicolas.leblanc
26

Możesz wywoływać metody prywatne . Jeśli wystąpił następujący błąd:

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

po prostu użyj // @ts-ignore:

// @ts-ignore
expect(new FooBar(/*...*/).initFooBar()).toEqual(/*...*/);
Mir-Ismaili
źródło
to powinno być na górze!
jsnewbie
2
To z pewnością inna opcja. Ma to ten sam problem as any, co utrata sprawdzania typu, w rzeczywistości tracisz sprawdzanie typu na całej linii.
Aaron Beall
19

Ponieważ większość programistów nie zaleca testowania funkcji prywatnej , dlaczego nie przetestować?

Na przykład.

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

Dzięki @Aaron, @Thierry Templier.

tymspy
źródło
1
Wydaje mi się, że maszynopis powoduje błędy podszywania się, gdy próbujesz wywołać metodę prywatną / chronioną.
Gudgip,
1
@Gudgip spowoduje błędy typu i nie będzie się kompilować. :)
tymspy
10

Nie pisz testów dla metod prywatnych. To pokonuje punkt testów jednostkowych.

  • Powinieneś testować publiczny interfejs API swojej klasy
  • NIE powinieneś testować szczegółów wcielania swojej klasy

Przykład

class SomeClass {

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

Test dla tej metody nie powinien wymagać zmiany, jeśli później implementacja ulegnie zmianie, ale behaviourpubliczny interfejs API pozostanie taki sam.

class SomeClass {

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

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

Nie upubliczniaj metod i właściwości tylko po to, aby je przetestować. Zazwyczaj oznacza to, że:

  1. Próbujesz przetestować implementację zamiast interfejsu API (interfejs publiczny).
  2. Powinieneś przenieść logikę do swojej klasy, aby ułatwić testowanie.
Jaskółka oknówka
źródło
3
Być może przeczytaj komentarz przed komentowaniem go. Wyraźnie stwierdzam i wykazuję, że testowanie szeregów prywatnych to zapach testowania implementacji, a nie zachowania, co prowadzi do kruchych testów.
Martin
1
Wyobraź sobie obiekt, który daje losową liczbę od 0 do prywatnej własności x. Jeśli chcesz wiedzieć, czy x jest poprawnie ustawiony przez konstruktora, znacznie łatwiej jest przetestować wartość x niż wykonać sto testów, aby sprawdzić, czy otrzymane liczby są w odpowiednim zakresie.
Galdor
1
@ user3725805 jest to przykład testowania implementacji, a nie zachowania. Lepiej byłoby wyodrębnić, skąd pochodzi liczba prywatna: stała, konfiguracja, konstruktor - i stamtąd. Jeśli szereg prywatny nie pochodzi z innego źródła, wówczas wpada w antypatertern „magiczna liczba”.
Martin
1
I dlaczego nie wolno testować wdrożenia? Testy jednostkowe pozwalają wykryć nieoczekiwane zmiany. Gdy z jakiegoś powodu konstruktor zapomni ustawić numer, test natychmiast kończy się niepowodzeniem i ostrzega mnie. Gdy ktoś zmienia implementację, test również kończy się niepowodzeniem, ale wolę zastosować jeden test, niż mieć niewykrywany błąd.
Galdor
2
+1. Świetna odpowiedź. @ TimJames Mówienie o prawidłowej praktyce lub wskazanie wadliwego podejścia jest właśnie celem SO. Zamiast znaleźć hacky kruchy sposób na osiągnięcie tego, czego chce OP.
Syed Aqeel Ashiq
4

Istotą „nie testuj metod prywatnych” jest tak naprawdę przetestowanie klasy jak ktoś, kto jej używa .

Jeśli masz publiczny interfejs API z 5 metodami, każdy konsument z Twojej klasy może z nich korzystać, dlatego powinieneś je przetestować. Konsument nie powinien uzyskiwać dostępu do prywatnych metod / właściwości twojej klasy, co oznacza, że ​​możesz zmieniać prywatnych członków, gdy publiczna funkcjonalność pozostanie taka sama.


Jeśli polegasz na wewnętrznej rozszerzalnej funkcjonalności, użyj protectedzamiast private.
Zauważ, że protectedwciąż jest publicznym interfejsem API (!) , Po prostu używany inaczej.

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

Testuj chronione właściwości w taki sam sposób, w jaki konsument wykorzystałby je, poprzez podklasę:

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
źródło
3

To działało dla mnie:

Zamiast:

sut.myPrivateMethod();

To:

sut['myPrivateMethod']();
mózgowiec
źródło
2

Przepraszam za nekro w tym poście, ale czuję się zmuszony zastanowić się nad kilkoma rzeczami, które nie wydają się być poruszone.

Przede wszystkim - kiedy podczas testów jednostkowych potrzebujemy dostępu do prywatnych członków klasy, jest to na ogół duża, gruba czerwona flaga, którą wyłudziliśmy w naszym strategicznym lub taktycznym podejściu i nieumyślnie naruszyliśmy jedną zasadę odpowiedzialności, popychając zachowanie, do którego nie należy. Poczucie potrzeby dostępu do metod, które są tak naprawdę niczym więcej niż izolowanym podprogramem procedury budowlanej, jest jednym z najczęstszych tego zjawisk; jednak to trochę tak, jakby twój szef spodziewał się, że przyjdziesz do pracy gotowy, a także mając przewrotną potrzebę wiedzieć, przez jaką poranną rutynę przeszedłeś, aby dostać się do tego stanu ...

Innym najczęstszym przykładem tego zdarzenia jest próba przetestowania przysłowiowej „klasy boga”. Jest to szczególny rodzaj problemu sam w sobie, ale cierpi z powodu tego samego podstawowego problemu z koniecznością poznania szczegółowych szczegółów procedury - ale to zejście z tematu.

W tym konkretnym przykładzie skutecznie przypisaliśmy odpowiedzialność za pełną inicjalizację obiektu Bar do konstruktora klasy FooBar. W programowaniu obiektowym jednym z głównych założeń jest to, że konstruktor jest „święty” i powinien być chroniony przed nieprawidłowymi danymi, które unieważniłyby jego „wewnętrzny stan i pozostawiałyby go do upadku gdzieś indziej (w czym może być bardzo głęboka) rurociąg.)

Nie udało nam się tego tutaj zrobić, pozwalając obiektowi FooBar zaakceptować pasek, który nie jest gotowy w momencie budowy FooBar, i zrekompensowaliśmy go poprzez „hakowanie” obiektu FooBar, aby wziąć sprawy w swoje „własne” ręce.

Wynika to z nieprzestrzegania innej zasady programowania obiektowego (w przypadku Bar), która polega na tym, że stan obiektu powinien być w pełni zainicjowany i gotowy do obsługi wszelkich połączeń przychodzących do jego członków publicznych natychmiast po utworzeniu. Nie oznacza to natychmiast po wywołaniu konstruktora we wszystkich przypadkach. Jeśli masz obiekt, który ma wiele złożonych scenariuszy konstrukcyjnych, lepiej jest wystawić setery do opcjonalnych elementów składowych na obiekt, który jest implementowany zgodnie ze wzorcem projektowym tworzenia (fabryka, konstruktor itp.) W dowolnym te ostatnie przypadki,

W twoim przykładzie właściwość „status” paska nie wydaje się być w prawidłowym stanie, w którym FooBar może to zaakceptować - więc FooBar robi coś, aby rozwiązać ten problem.

Drugi problem, jaki widzę, polega na tym, że wydaje się, że próbujesz przetestować swój kod, zamiast ćwiczyć programowanie oparte na testach. To zdecydowanie moja własna opinia w tym momencie; ale tego typu testy są naprawdę anty-wzorcem. W końcu wpadasz w pułapkę uświadomienia sobie, że masz podstawowe problemy z projektowaniem, które uniemożliwiają testowanie kodu po fakcie, zamiast pisania potrzebnych testów, a następnie programowania do testów. Niezależnie od tego, jak podejdziesz do problemu, powinieneś skończyć z taką samą liczbą testów i linii kodu, jeśli naprawdę osiągnąłeś implementację SOLID. Więc - po co próbować odwracać swoją drogę do kodu, który można przetestować, skoro możesz po prostu rozwiązać ten problem na początku swoich prac programistycznych?

Gdybyś to zrobił, znacznie wcześniej zdałbyś sobie sprawę, że będziesz musiał napisać dość nieprzyjemny kod, aby przetestować swój projekt, i miałbyś możliwość wcześniejszego dostosowania swojego podejścia poprzez zmianę zachowania na implementacje, które są łatwe do przetestowania.

Ryan Hansen
źródło
2

Zgadzam się z @toskv: Nie polecam tego robić :-)

Ale jeśli naprawdę chcesz przetestować swoją prywatną metodę, możesz mieć świadomość, że odpowiedni kod dla TypeScript odpowiada metodzie prototypu funkcji konstruktora. Oznacza to, że można go używać w czasie wykonywania (podczas gdy prawdopodobnie wystąpią pewne błędy kompilacji).

Na przykład:

export class FooBar {
  private _status: number;

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

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

zostaną przełożone na:

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

Zobacz ten fragment: https://plnkr.co/edit/calJCF?p=preview .

Thierry Templier
źródło
1

Jak już wielu z nich stwierdziło, tak bardzo, jak chcesz przetestować metody prywatne, nie powinieneś hakować kodu ani transpilatora, aby działał on dla Ciebie. Współczesny TypeScript odrzuci większość hacków, które ludzie udostępnili do tej pory.


Rozwiązanie

TLDR ; jeśli metoda powinna zostać przetestowana, należy oddzielić kod do klasy, dzięki czemu można udostępnić metodę do publicznego przetestowania.

Powodem, dla którego masz metodę prywatną, jest to, że funkcjonalność niekoniecznie musi być ujawniona przez tę klasę, a zatem jeśli funkcjonalność tam nie należy, powinna zostać oddzielona od swojej klasy.

Przykład

Natknąłem się na ten artykuł, który świetnie wyjaśnia, w jaki sposób należy radzić sobie z testowaniem prywatnych metod. Obejmuje nawet niektóre metody tutaj i dlaczego są to złe implementacje.

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

Uwaga : ten kod jest usuwany z blogu, do którego prowadzi link powyżej (powielam się na wypadek zmiany treści za linkiem)

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

    private getUserAddress(){
        //...
        this.formatStreet();
        //...
    }
    private formatStreet(){
        //...
    }
}
Po
class User{
    private address:Address;
    public getUserInformationToDisplay(){
        //...
        address.getUserAddress();
        //...
    }
}
class Address{
    private format: StreetFormatter;
    public format(){
        //...
        format.ToString();
        //...
    }
}
class StreetFormatter{
    public toString(){
        // ...
    }
}
CTS_AE
źródło
1

wywołaj metodę prywatną za pomocą nawiasów kwadratowych

Plik Ts

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

plik 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
źródło
0

Odpowiedź Aarona jest najlepsza i działa dla mnie :) Głosowałbym, ale niestety nie mogę (brak reputacji).

Muszę powiedzieć, że testowanie prywatnych metod jest jedynym sposobem na ich użycie i uzyskanie czystego kodu po drugiej stronie.

Na przykład:

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

„Sensowne jest nie testowanie wszystkich tych metod naraz, ponieważ musielibyśmy wyśmiewać te prywatne metody, których nie możemy wyszydzić, ponieważ nie mamy do nich dostępu. Oznacza to, że potrzebujemy dużo konfiguracji do testu jednostkowego, aby przetestować to jako całość.

To powiedziawszy, najlepszym sposobem przetestowania powyższej metody ze wszystkimi zależnościami jest test kompleksowy, ponieważ tutaj potrzebny jest test integracyjny, ale test E2E nie pomoże ci, jeśli ćwiczysz TDD (Test Driven Development), ale testowanie każda metoda będzie.

Devpool
źródło
0

Ta droga, którą wybieram, jest tą, w której tworzę funkcje poza klasą i przypisuję funkcję do mojej prywatnej metody.

export class MyClass {
  private _myPrivateFunction = someFunctionThatCanBeTested;
}

function someFunctionThatCanBeTested() {
  //This Is Testable
}

Teraz nie wiem, jaki rodzaj reguł OOP łamię, ale aby odpowiedzieć na pytanie, w ten sposób testuję prywatne metody. Z radością witam każdego, kto doradzi na ten temat.

Sani Yusuf
źródło