Cara menulis unit testing untuk Angular / TypeScript untuk metode pribadi dengan Jasmine

196

Bagaimana Anda menguji fungsi pribadi di sudut 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;
    }

}

Solusi yang saya temukan

  1. Letakkan kode tes itu sendiri di dalam penutupan atau Tambahkan kode di dalam penutupan yang menyimpan referensi ke variabel lokal pada objek yang ada di lingkup luar.

    Kemudian hapus kode uji menggunakan alat. http://philipwalton.com/articles/how-to-unit-test-private-functions-in-javascript/

Tolong sarankan saya cara yang lebih baik untuk menyelesaikan masalah ini jika Anda telah melakukan apa pun?

PS

  1. Sebagian besar jawaban untuk jenis pertanyaan serupa seperti ini tidak memberikan solusi untuk masalah, itu sebabnya saya menanyakan pertanyaan ini

  2. Sebagian besar pengembang mengatakan Anda Jangan menguji fungsi pribadi tetapi saya tidak mengatakan mereka salah atau benar, tetapi ada kebutuhan untuk kasus saya untuk menguji pribadi.

tymspy
sumber
11
tes seharusnya hanya menguji antarmuka publik, bukan implementasi pribadi. Tes yang Anda lakukan pada antarmuka publik juga harus mencakup bagian pribadi.
toskv
16
Saya suka bagaimana setengah dari jawaban yang seharusnya adalah komentar. OP mengajukan pertanyaan, bagaimana Anda X? Jawaban yang diterima sebenarnya memberitahu Anda bagaimana melakukan X. Kemudian sebagian besar sisanya berbalik dan berkata, tidak hanya saya tidak akan memberi tahu Anda X (yang jelas mungkin) tetapi Anda harus melakukan Y. Sebagian besar alat pengujian unit (saya tidak berbicara tentang hanya JavaScript di sini) mampu menguji fungsi / metode pribadi. Saya akan terus menjelaskan mengapa karena tampaknya telah hilang di tanah JS (tampaknya, diberikan setengah jawaban).
Quaternion
13
Adalah praktik pemrograman yang baik untuk memecah masalah menjadi tugas yang dapat dikelola, jadi fungsi "foo (x: type)" akan memanggil fungsi pribadi a (x: type), b (x: type), c (y: another_type) dan d ( z: yet_another_type). Sekarang karena foo, mengelola panggilan dan menyatukan semuanya, itu menciptakan semacam turbulensi, seperti sisi belakang batu dalam aliran, bayangan yang sangat sulit untuk memastikan semua rentang diuji. Dengan demikian lebih mudah untuk memastikan bahwa setiap sub rangkaian rentang valid, jika Anda mencoba menguji induk "foo" saja, pengujian rentang menjadi sangat rumit dalam kasus.
Quaternion
18
Ini bukan untuk mengatakan Anda tidak menguji antarmuka publik, tentu saja Anda lakukan, tetapi menguji metode pribadi memungkinkan Anda untuk menguji serangkaian potongan dikelola pendek (alasan yang sama Anda menulisnya di tempat pertama, mengapa Anda membatalkan ini ketika datang ke pengujian), dan hanya karena tes pada antarmuka publik valid (mungkin fungsi panggilan membatasi rentang input) tidak berarti bahwa metode pribadi tidak cacat ketika Anda menambahkan logika yang lebih maju dan memanggil mereka dari yang lain fungsi induk baru,
Quaternion
5
jika Anda mengujinya dengan TDD, Anda tidak akan mencoba mencari tahu apa yang Anda lakukan nanti, ketika Anda harus mengujinya dengan benar.
Quaternion

Jawaban:

343

Saya bersama Anda, meskipun itu adalah tujuan yang baik untuk "hanya menguji unit API publik" ada kalanya tampaknya tidak sesederhana itu dan Anda merasa Anda memilih antara mengkompromikan API atau unit-test. Anda sudah tahu ini, karena itulah yang ingin Anda lakukan, jadi saya tidak akan membahasnya. :)

Di TypeScript saya telah menemukan beberapa cara Anda dapat mengakses anggota pribadi demi pengujian unit. Pertimbangkan kelas ini:

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

}

Meskipun akses TS Membatasi kepada anggota kelas menggunakan private, protected, public, yang disusun JS tidak memiliki anggota pribadi, karena ini bukan hal yang di JS. Ini murni digunakan untuk kompiler TS. Untuk itu:

  1. Anda dapat menegaskan anydan keluar dari kompiler agar tidak memperingatkan Anda tentang batasan akses:

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

    Masalah dengan pendekatan ini adalah bahwa kompiler tidak tahu apa yang Anda lakukan dengan benar any, sehingga Anda tidak mendapatkan kesalahan tipe yang diinginkan:

    (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

    Ini jelas akan membuat refactoring lebih sulit.

  2. Anda dapat menggunakan akses array ( []) untuk mendapatkan anggota pribadi:

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

    Meskipun terlihat funky, TSC sebenarnya akan memvalidasi jenis-jenisnya seolah-olah Anda mengaksesnya secara langsung:

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

    Sejujurnya saya tidak tahu mengapa ini berhasil. Ini tampaknya merupakan "pintu keluar" yang disengaja untuk memberi Anda akses ke anggota pribadi tanpa kehilangan jenis keamanan. Ini persis seperti yang saya pikir Anda inginkan untuk pengujian unit Anda.

Berikut ini adalah contoh yang berfungsi di TypeScript Playground .

Edit untuk TypeScript 2.6

Opsi lain yang beberapa suka adalah menggunakan // @ts-ignore( ditambahkan pada TS 2.6 ) yang hanya menekan semua kesalahan pada baris berikut:

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

Masalah dengan ini adalah, yah, itu menekan semua kesalahan pada baris berikut:

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

Saya pribadi mempertimbangkan @ts-ignorebau kode, dan seperti yang dikatakan oleh para dokter:

kami sarankan Anda menggunakan komentar ini dengan sangat hemat . [penekanan asli]

Aaron Beall
sumber
45
Sangat menyenangkan untuk mendengar sikap realistis pada pengujian unit bersama dengan solusi aktual daripada dogma tester unit standar.
d512
2
Beberapa penjelasan "resmi" dari perilaku (yang bahkan mengutip pengujian unit sebagai kasus penggunaan): github.com/microsoft/TypeScript/issues/19335
Aaron Beall
1
Cukup gunakan `// @ ts-abaikan` seperti yang ditunjukkan di bawah ini. untuk memberitahu linter untuk mengabaikan accessor pribadi
Tommaso
1
@Tommaso Ya, itu opsi lain, tetapi memiliki kelemahan yang sama dalam menggunakan as any: Anda kehilangan semua pemeriksaan jenis.
Aaron Beall
2
Jawaban terbaik yang pernah saya lihat, terima kasih @AaronBeall. Dan juga, terima kasih tymspy untuk menanyakan pertanyaan aslinya.
nicolas.leblanc
26

Anda dapat memanggil metode pribadi . Jika Anda mengalami kesalahan berikut:

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

cukup gunakan // @ts-ignore:

// @ts-ignore
expect(new FooBar(/*...*/).initFooBar()).toEqual(/*...*/);
Mir-Ismaili
sumber
ini harus di atas!
jsnewbie
2
Ini tentu pilihan lain. Itu menderita masalah yang sama seperti as anyAnda kehilangan pemeriksaan jenis, sebenarnya Anda kehilangan pemeriksaan jenis di seluruh baris.
Aaron Beall
19

Karena sebagian besar pengembang tidak merekomendasikan pengujian fungsi pribadi , Mengapa tidak mengujinya ?.

Misalnya.

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

Terima kasih kepada @ Harun, @Thierry Templier.

tymspy
sumber
1
Saya pikir naskah memberikan kesalahan linting ketika Anda mencoba memanggil metode pribadi / dilindungi.
Gudgip
1
@ Gudgip itu akan memberikan kesalahan ketik dan tidak akan dikompilasi. :)
tymspy
10

Jangan menulis tes untuk metode pribadi. Ini mengalahkan titik tes unit.

  • Anda harus menguji API publik kelas Anda
  • Anda TIDAK boleh menguji rincian penindasan kelas Anda

Contoh

class SomeClass {

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

Tes untuk metode ini seharusnya tidak perlu berubah jika nanti implementasinya berubah tetapi behaviourAPI publik tetap sama.

class SomeClass {

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

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

Jangan membuat metode dan properti menjadi publik hanya untuk mengujinya. Ini biasanya berarti bahwa:

  1. Anda mencoba menguji implementasi alih-alih API (antarmuka publik).
  2. Anda harus memindahkan logika yang dipermasalahkan ke dalam kelasnya sendiri untuk membuat pengujian lebih mudah.
Martin
sumber
3
Mungkin membaca posting sebelum mengomentarinya. Saya dengan jelas menyatakan dan menunjukkan bahwa kemaluan pengujian adalah bau pelaksanaan pengujian daripada perilaku, yang mengarah pada tes rapuh.
Martin
1
Bayangkan sebuah objek yang memberi Anda angka acak antara 0 dan properti pribadi x. Jika Anda ingin tahu apakah x diatur dengan benar oleh konstruktor, jauh lebih mudah untuk menguji nilai x daripada membuat seratus tes untuk memeriksa apakah angka yang Anda dapatkan berada dalam kisaran yang tepat.
Galdor
1
@ user3725805 ini adalah contoh pengujian implementasi, bukan perilaku. Akan lebih baik untuk mengisolasi dari mana nomor pribadi itu berasal: sebuah konstanta, sebuah konfigurasi, konstruktor - dan uji dari sana. Jika private tidak berasal dari sumber lain, maka ia jatuh ke antipattern "angka ajaib".
Martin
1
Dan mengapa tidak diizinkan untuk menguji implementasi? Tes unit bagus untuk mendeteksi perubahan yang tidak terduga. Ketika karena alasan tertentu konstruktor lupa untuk mengatur nomor, tes gagal segera dan memperingatkan saya. Ketika seseorang mengubah implementasi, tes juga gagal, tetapi saya lebih memilih untuk mengadopsi satu tes daripada memiliki kesalahan yang tidak terdeteksi.
Galdor
2
+1. Jawaban yang bagus @TimJames Memberi tahu praktik yang benar atau menunjukkan pendekatan yang salah adalah tujuan SO. Alih-alih menemukan cara rapuh yang rapuh untuk mencapai apa pun yang diinginkan OP.
Syed Aqeel Ashiq
4

Inti dari "jangan menguji metode pribadi" sebenarnya adalah Menguji kelas seperti seseorang yang menggunakannya .

Jika Anda memiliki API publik dengan 5 metode, konsumen kelas Anda dapat menggunakannya, dan karenanya Anda harus mengujinya. Seorang konsumen tidak boleh mengakses metode / properti pribadi kelas Anda, yang berarti Anda dapat mengubah anggota pribadi ketika fungsi publik yang terbuka tetap sama.


Jika Anda mengandalkan fungsionalitas ekstensible internal, gunakan protectedsebagai ganti private.
Perhatikan bahwa protectedini masih API publik (!) , Hanya digunakan secara berbeda.

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

Uji unit properti yang dilindungi dengan cara yang sama seperti konsumen akan menggunakannya, melalui subklas:

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
sumber
3

Ini bekerja untuk saya:

Dari pada:

sut.myPrivateMethod();

Ini:

sut['myPrivateMethod']();
pembuat otak
sumber
2

Maaf untuk necro di posting ini, tapi saya merasa harus mempertimbangkan beberapa hal yang tampaknya belum disentuh.

Pertama yang terpenting - ketika kita menemukan diri kita membutuhkan akses ke anggota pribadi di kelas selama pengujian unit, umumnya merupakan bendera merah besar dan gemuk yang telah kita abaikan dalam pendekatan strategis atau taktis kita dan secara tidak sengaja telah melanggar prinsip tanggung jawab tunggal dengan mendorong perilaku di tempat yang tidak seharusnya. Merasakan perlunya mengakses metode yang benar-benar tidak lebih dari subrutin terisolasi dari prosedur konstruksi adalah salah satu kejadian paling umum dari ini; Namun, ini seperti bos Anda yang mengharapkan Anda datang ke tempat kerja siap-pergi dan juga memiliki beberapa kebutuhan buruk untuk mengetahui rutinitas pagi apa yang Anda lalui untuk membawa Anda ke keadaan itu ...

Contoh paling umum lainnya dari kejadian ini adalah ketika Anda mencoba menguji "kelas dewa" pepatah. Ini adalah jenis masalah khusus di dalam dan dari dirinya sendiri, tetapi menderita dari masalah dasar yang sama dengan perlu mengetahui detail intim dari suatu prosedur - tapi itu keluar dari topik.

Dalam contoh khusus ini, kami telah secara efektif menetapkan tanggung jawab menginisialisasi sepenuhnya objek Bar ke konstruktor kelas FooBar. Dalam pemrograman berorientasi objek, salah satu penyewa inti adalah bahwa konstruktornya "sakral" dan harus dijaga terhadap data yang tidak valid yang akan membuat keadaan internal tidak valid dan membiarkannya gagal di tempat lain di hilir (dalam apa yang bisa menjadi sangat dalam) pipa.)

Kami gagal melakukannya di sini dengan mengizinkan objek FooBar menerima Bilah yang belum siap pada saat FooBar dibangun, dan telah mengompensasi dengan semacam "peretasan" objek FooBar untuk mengambil masalah menjadi miliknya sendiri tangan.

Ini adalah hasil dari kegagalan untuk mematuhi penyewa lain pemrograman berorientasi objek (dalam kasus Bar,) yang menyatakan bahwa keadaan objek harus sepenuhnya diinisialisasi dan siap untuk menangani panggilan masuk apa pun ke anggota publiknya segera setelah pembuatan. Sekarang, ini tidak berarti segera setelah konstruktor dipanggil dalam semua contoh. Ketika Anda memiliki objek yang memiliki banyak skenario konstruksi kompleks, maka lebih baik untuk mengekspos setter ke anggota opsionalnya ke objek yang diimplementasikan sesuai dengan pola desain-penciptaan (Pabrik, Pembangun, dll ...) Di salah satu kasus terakhir,

Dalam contoh Anda, properti "status" Bar tampaknya tidak dalam keadaan valid di mana FooBar dapat menerimanya - sehingga FooBar melakukan sesuatu untuk memperbaikinya.

Masalah kedua yang saya lihat adalah tampaknya Anda mencoba menguji kode Anda daripada mempraktikkan pengembangan yang digerakkan oleh tes. Ini jelas merupakan pendapat saya sendiri pada saat ini; tetapi, jenis pengujian ini benar-benar anti-pola. Apa yang akhirnya Anda lakukan adalah jatuh ke dalam perangkap menyadari bahwa Anda memiliki masalah desain inti yang mencegah kode Anda dari diuji setelah fakta, daripada menulis tes yang Anda butuhkan dan kemudian pemrograman untuk tes. Apa pun masalah yang Anda hadapi, Anda masih harus berakhir dengan jumlah tes dan jalur kode yang sama jika Anda benar-benar mencapai implementasi SOLID. Jadi - mengapa mencoba dan merekayasa balik jalan Anda menjadi kode yang dapat diuji ketika Anda bisa mengatasi masalah tersebut saat upaya pengembangan Anda dimulai?

Seandainya Anda melakukan itu, maka Anda akan menyadari jauh sebelumnya bahwa Anda harus menulis beberapa kode yang agak menjengkelkan untuk menguji desain Anda dan akan memiliki kesempatan sejak awal untuk menyelaraskan kembali pendekatan Anda dengan mengubah perilaku menjadi implementasi yang mudah diuji.

Ryan Hansen
sumber
2

Saya setuju dengan @toskv: Saya tidak akan merekomendasikan untuk melakukan itu :-)

Tetapi jika Anda benar-benar ingin menguji metode pribadi Anda, Anda dapat menyadari bahwa kode yang sesuai untuk TypeScript sesuai dengan metode prototipe fungsi konstruktor. Ini berarti dapat digunakan saat runtime (padahal Anda mungkin akan memiliki beberapa kesalahan kompilasi).

Sebagai contoh:

export class FooBar {
  private _status: number;

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

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

akan diubah menjadi:

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

Lihat plunkr ini: https://plnkr.co/edit/calJCF?p=preview .

Thierry Templier
sumber
1

Seperti yang telah banyak dinyatakan, sebanyak Anda ingin menguji metode pribadi Anda tidak harus meretas kode atau transpiler Anda untuk membuatnya bekerja untuk Anda. TypeScript modern akan menolak sebagian besar peretasan yang telah disediakan orang sejauh ini.


Larutan

TLDR ; jika suatu metode harus diuji maka Anda harus memisahkan kode ke dalam kelas yang Anda dapat mengekspos metode tersebut untuk umum untuk diuji.

Alasan Anda memiliki metode privat adalah karena fungsionalitas tidak harus menjadi milik diekspos oleh kelas itu, dan oleh karena itu jika fungsionalitas tidak termasuk di sana itu harus dipisahkan ke dalam kelas itu sendiri.

Contoh

Saya membaca artikel ini yang menjelaskan cara Anda menangani metode pribadi. Bahkan mencakup beberapa metode di sini dan bagaimana mengapa itu implementasi yang buruk.

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

Catatan : Kode ini diambil dari blog yang ditautkan di atas (Saya menduplikasi jika konten di balik tautan berubah)

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

    private getUserAddress(){
        //...
        this.formatStreet();
        //...
    }
    private formatStreet(){
        //...
    }
}
Setelah
class User{
    private address:Address;
    public getUserInformationToDisplay(){
        //...
        address.getUserAddress();
        //...
    }
}
class Address{
    private format: StreetFormatter;
    public format(){
        //...
        format.ToString();
        //...
    }
}
class StreetFormatter{
    public toString(){
        // ...
    }
}
CTS_AE
sumber
1

panggil metode pribadi menggunakan tanda kurung

File ts

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

file 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
sumber
0

Jawaban oleh Aaron adalah yang terbaik dan bekerja untuk saya :) Saya akan memilihnya tetapi sayangnya saya tidak bisa (kehilangan reputasi).

Saya harus mengatakan pengujian metode pribadi adalah satu-satunya cara untuk menggunakannya dan memiliki kode bersih di sisi lain.

Sebagai contoh:

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

Sangat masuk akal untuk tidak menguji semua metode ini sekaligus karena kita perlu mencemooh metode pribadi itu, yang tidak dapat kita tiru karena kita tidak dapat mengaksesnya. Ini berarti kita membutuhkan banyak konfigurasi untuk pengujian unit untuk menguji ini secara keseluruhan.

Ini mengatakan cara terbaik untuk menguji metode di atas dengan semua dependensi adalah tes ujung ke ujung, karena di sini tes integrasi diperlukan, tetapi tes E2E tidak akan membantu Anda jika Anda berlatih TDD (Test Driven Development), tetapi pengujian metode apa pun akan.

Devpool
sumber
0

Rute ini saya ambil adalah salah satu tempat saya membuat fungsi di luar kelas dan menetapkan fungsi ke metode pribadi saya.

export class MyClass {
  private _myPrivateFunction = someFunctionThatCanBeTested;
}

function someFunctionThatCanBeTested() {
  //This Is Testable
}

Sekarang saya tidak tahu apa jenis aturan OOP yang saya langgar, tetapi untuk menjawab pertanyaan, ini adalah cara saya menguji metode pribadi. Saya menyambut siapa pun untuk memberi nasihat tentang Pro & Kontra ini.

Sani Yusuf
sumber