Bagaimana cara menulis unit test sebelum refactoring?


55

Saya telah membaca beberapa jawaban untuk pertanyaan di sepanjang baris yang sama seperti "Bagaimana Anda menjaga tes unit Anda bekerja saat refactoring?". Dalam kasus saya, skenarionya sedikit berbeda karena saya diberi proyek untuk meninjau dan menyesuaikan dengan beberapa standar yang kami miliki, saat ini tidak ada tes sama sekali untuk proyek!

Saya telah mengidentifikasi beberapa hal yang saya pikir bisa dilakukan dengan lebih baik seperti TIDAK mencampur kode jenis DAO dalam lapisan layanan.

Sebelum refactoring, sepertinya ide yang baik untuk menulis tes untuk kode yang ada. Masalahnya bagi saya adalah ketika saya melakukan refactor maka tes-tes itu akan rusak ketika saya mengubah di mana logika tertentu dilakukan dan tes akan ditulis dengan struktur sebelumnya dalam pikiran (mocked dependency, dll.)

Dalam kasus saya, apa cara terbaik untuk melanjutkan? Saya tergoda untuk menulis tes di sekitar kode refactored tapi saya sadar ada risiko saya dapat memperbaiki hal-hal yang salah yang dapat mengubah perilaku yang diinginkan.

Apakah ini refactor atau redesign, saya senang pengertian saya tentang istilah-istilah itu diperbaiki, saat ini saya sedang mengerjakan definisi berikut untuk refactoring "Dengan refactoring, menurut definisi, Anda tidak mengubah apa yang dilakukan perangkat lunak Anda, Anda mengubah cara kerjanya. " Jadi saya tidak mengubah perangkat lunak apa yang akan saya ubah bagaimana / di mana ia melakukannya.

Sama saya bisa melihat argumen bahwa jika saya mengubah tanda tangan metode yang dapat dianggap desain ulang.

Ini contoh singkatnya

MyDocumentService.java (arus)

public class MyDocumentService {
   ...
   public List<Document> findAllDocuments() {
      DataResultSet rs = documentDAO.findAllDocuments();
      List<Document> documents = new ArrayList<>();
      for(DataObject do: rs.getRows()) {
         //get row data create new document add it to 
         //documents list
      }

      return documents;
   }
}

MyDocumentService.java (refactored / didesain ulang apa pun)

public class MyDocumentService {
   ...
   public List<Document> findAllDocuments() {
      //Code dealing with DataResultSet moved back up to DAO
      //DAO now returns a List<Document> instead of a DataResultSet
      return documentDAO.findAllDocuments();
   }
}

14
Apakah ini benar-benar refactoring yang Anda rencanakan, atau desain ulang ? Karena jawabannya mungkin berbeda dalam dua kasus.
herby

4
Saya sedang mengerjakan definisi "Dengan refactoring, menurut definisi, Anda tidak mengubah apa yang dilakukan perangkat lunak Anda, Anda mengubah cara kerjanya." Jadi saya percaya dalam kasus ini adalah refactoring, jangan ragu untuk memperbaiki pemahaman saya tentang istilah ini
PDStat

21
Jangan, tulis tes integrasi. "Refactoring" yang Anda rencanakan berada di atas tingkat pengujian unit. Hanya unit yang menguji kelas baru (atau yang lama yang Anda tahu Anda pelihara).
Stop Harming Monica

2
Sehubungan dengan definisi refactoring, apakah perangkat lunak Anda mendefinisikan dengan jelas apa fungsinya? Dengan kata lain, apakah sudah "dimasukkan" ke dalam modul dengan API independen? Jika tidak, maka Anda tidak dapat melakukan refactor, kecuali mungkin pada level tertinggi (yang menghadap pengguna). Pada level modul, Anda pasti akan mendesain ulang itu. Dalam hal ini, jangan buang waktu Anda menulis tes unit sebelum Anda memiliki unit.
Kevin Krumwiede

4
Anda kemungkinan besar harus melakukan sedikit refactoring tanpa jaring pengaman dari tes hanya untuk bisa membuatnya menjadi test harness. Saran terbaik yang bisa saya berikan kepada Anda adalah bahwa jika IDE atau alat refactoring Anda tidak akan melakukannya untuk Anda, jangan lakukan itu dengan tangan. Terus menerapkan refactoring otomatis sampai Anda dapat memanfaatkan CUT. Anda pasti ingin mengambil salinan Michael Feather "Bekerja Efektif dengan Kode Legacy".
RubberDuck

Jawaban:


56

Anda sedang mencari tes yang memeriksa regresi . yaitu melanggar beberapa perilaku yang ada. Saya akan mulai dengan mengidentifikasi pada tingkat apa perilaku itu akan tetap sama, dan bahwa antarmuka yang mendorong perilaku itu akan tetap sama, dan mulai melakukan tes pada saat itu.

Anda sekarang memiliki beberapa tes yang akan menyatakan bahwa apa pun yang Anda lakukan di bawah level ini, perilaku Anda tetap sama.

Anda benar mempertanyakan bagaimana tes dan kode dapat tetap sinkron. Jika antarmuka Anda ke komponen tetap sama, maka Anda dapat menulis tes di sekitar ini dan menyatakan kondisi yang sama untuk kedua implementasi (saat Anda membuat implementasi baru). Jika tidak, maka Anda harus menerima bahwa tes untuk komponen yang redundan adalah tes yang berlebihan.


1
Yaitu, Anda mungkin akan melakukan integrasi atau pengujian sistem daripada pengujian unit. Anda mungkin masih akan menggunakan alat "unit testing" untuk itu, tetapi Anda akan memukul lebih dari satu unit kode dengan setiap tes.
Moz

Iya. Itulah masalahnya. Tes regresi Anda mungkin bisa melakukan sesuatu yang sangat tinggi misalnya permintaan REST ke server dan mungkin tes database berikutnya (yaitu jelas bukan tes unit !)
Brian Agnew

40

Praktik yang disarankan adalah memulai dengan menulis "tes pin-down" yang menguji perilaku kode saat ini, mungkin termasuk bug, tetapi tanpa mengharuskan Anda untuk masuk ke dalam kegilaan membedakan apakah perilaku tertentu yang melanggar dokumen persyaratan adalah bug, solusi untuk sesuatu yang tidak Anda sadari, atau merupakan perubahan persyaratan yang tidak didokumentasikan.

Masuk akal jika tes pin-down ini dilakukan pada level tinggi, yaitu integrasi daripada tes unit, sehingga tes tersebut akan tetap berfungsi saat Anda mulai melakukan refactoring.

Tetapi beberapa refactoring mungkin diperlukan untuk membuat kode dapat diuji - hanya berhati-hatilah untuk tetap pada refactoring "aman". Misalnya, dalam hampir semua kasus, metode yang bersifat pribadi dapat dipublikasikan tanpa melanggar apa pun.


+1 untuk tes integrasi. Bergantung pada aplikasinya, Anda mungkin bisa mulai pada level mengirimkan permintaan ke aplikasi web. Apa yang dikirim kembali oleh aplikasi tidak boleh berubah hanya karena refactoring, meskipun jika itu mengirim kembali HTML, ini tentu kurang dapat diuji.
jpmc26

Saya suka tes frase 'dijabarkan'.
Brian Agnew

12

Saya sarankan - jika Anda belum - membaca baik Bekerja Efektif Dengan Kode Legacy maupun Refactoring - Meningkatkan Desain Kode yang Ada .

[..] Masalahnya bagi saya adalah ketika saya melakukan refactor maka tes-tes itu akan pecah ketika saya berubah ketika logika tertentu dilakukan dan tes akan ditulis dengan struktur sebelumnya dalam pikiran (mocked dependency, dll.) [ ..]

Saya tidak perlu melihat ini sebagai masalah: Tulis tes, ubah struktur kode Anda, dan kemudian sesuaikan struktur tes juga . Ini akan memberi Anda umpan balik langsung apakah struktur baru Anda sebenarnya lebih baik daripada yang lama, karena jika ya, tes yang disesuaikan akan lebih mudah untuk ditulis (dan dengan demikian mengubah tes harus relatif mudah, menurunkan risiko memiliki yang baru diperkenalkan bug lulus tes).

Juga, seperti yang telah ditulis orang lain: Jangan menulis tes terlalu rinci (setidaknya tidak di awal). Cobalah untuk tetap berada pada tingkat abstraksi yang tinggi (dengan demikian tes Anda mungkin akan lebih baik ditandai sebagai regresi atau bahkan tes integrasi).


1
Ini. Tes akan terlihat mengerikan , tetapi akan mencakup perilaku yang ada. Kemudian, saat kode dihidupkan kembali, lakukan juga tes, pada langkah kunci. Ulangi sampai Anda memiliki sesuatu yang Anda banggakan. ++
RubberDuck

1
Saya kedua rekomendasi buku itu - saya selalu dekat ketika saya harus berurusan dengan kode testless.
Toby Speight

5

Jangan menulis tes unit ketat di mana Anda mengejek semua dependensi. Beberapa orang akan memberi tahu Anda ini bukan tes unit nyata. Abaikan mereka. Tes ini bermanfaat, dan itulah yang penting.

Mari kita lihat contoh Anda:

public class MyDocumentService {
   ...
   public List<Document> findAllDocuments() {
      DataResultSet rs = documentDAO.findAllDocuments();
      List<Document> documents = new ArrayList<>();
      for(DataObject do: rs.getRows()) {
         //get row data create new document add it to 
         //documents list
      }

      return documents;
   }
}

Tes Anda mungkin terlihat seperti ini:

DocumentDao documentDao = Mock.create(DocumentDao.class);
Mock.when(documentDao.findAllDocuments())
    .thenReturn(DataResultSet.create(...))
assertEquals(..., new MyDocumentService(documentDao).findAllDocuments());

Alih-alih mengejek DocumentPOR, mengejek dependensinya:

DocumentDao documentDao = new DocumentDao(db);
Mock.when(db...)
    .thenReturn(...)
assertEquals(..., new MyDocumentService(documentDao).findAllDocuments());

Sekarang, Anda dapat memindahkan logika dari MyDocumentServiceke DocumentDaotanpa melanggar tes. Tes akan menunjukkan bahwa fungsinya sama (sejauh Anda telah mengujinya).


Jika Anda menguji DocumentService dan tidak mengejek DAO, itu bukan tes unit sama sekali. Ini adalah sesuatu di antara uji kesatuan dan integrasi. Bukankah begitu?
Laiv

7
@ Longv, sebenarnya ada variasi yang signifikan dalam cara orang menggunakan istilah unit test. Beberapa menggunakannya hanya berarti tes yang sangat terisolasi. Lainnya termasuk tes apa pun yang berjalan cepat. Beberapa termasuk apa pun yang berjalan dalam kerangka uji. Tetapi pada akhirnya, tidak masalah bagaimana Anda ingin mendefinisikan istilah unit test. Pertanyaannya adalah tes apa yang berguna, jadi kita tidak boleh terganggu oleh bagaimana tepatnya kita mendefinisikan unit test.
Winston Ewert

Poin luar biasa yang menunjukkan bahwa kegunaan adalah apa yang paling penting. Unit test extravagant untuk algoritma paling sepele hanya demi memiliki unit test lebih berbahaya daripada baik, jika tidak hanya buang-buang waktu dan sumber daya yang berharga. Ini dapat diterapkan pada hampir semua hal dan merupakan sesuatu yang saya harap saya ketahui sebelumnya dalam karir saya.
Lee

3

Seperti yang Anda katakan, jika Anda mengubah perilaku maka itu adalah transformasi dan bukan refactor. Pada tingkat apa Anda mengubah perilaku adalah apa yang membuat perbedaan.

Jika tidak ada tes formal di level tertinggi maka cobalah dan temukan satu set persyaratan yang klien (kode panggilan atau manusia) yang perlu tetap sama setelah Anda mendesain ulang agar kode Anda dianggap berfungsi. Itu adalah daftar kasus uji yang perlu Anda laksanakan.

Untuk menjawab pertanyaan Anda tentang perubahan implementasi yang membutuhkan perubahan test case, saya sarankan Anda melihat Detroit (klasik) vs London (mockist) TDD. Martin Fowler membicarakan hal ini dalam artikelnya yang hebat Mocks bukan bertopik tetapi banyak orang memiliki pendapat. Jika Anda mulai di level tertinggi, di mana eksternal Anda tidak bisa berubah, dan turun, maka persyaratannya harus tetap stabil sampai Anda mencapai level yang benar-benar perlu diubah.

Tanpa tes apa pun, ini akan sulit, dan Anda mungkin ingin mempertimbangkan menjalankan klien melalui jalur kode ganda (dan mencatat perbedaannya) sampai Anda dapat memastikan kode baru Anda melakukan apa yang perlu dilakukan.


3

Ini pendekatan saya. Ini memiliki biaya dalam hal waktu karena merupakan tes refactor dalam 4 fase.

Apa yang akan saya paparkan mungkin cocok dengan komponen yang lebih rumit daripada yang terpapar dalam contoh pertanyaan.

Bagaimanapun strategi tersebut valid untuk setiap kandidat komponen yang dinormalisasi dengan antarmuka (DAO, Layanan, Pengendali, ...).

1. Antarmuka

Mari kita kumpulkan semua metode publik dari MyDocumentService dan mari kita satukan semuanya menjadi sebuah antarmuka. Misalnya. Jika sudah ada, gunakan yang itu daripada mengatur yang baru .

public interface DocumentService {

   List<Document> getAllDocuments();

   //more methods here...
}

Kemudian kami memaksa MyDocumentService untuk mengimplementasikan antarmuka baru ini.

Sejauh ini bagus. Tidak ada perubahan besar yang dilakukan, kami menghormati kontrak saat ini dan behaivos tetap tidak tersentuh.

public class MyDocumentService implements DocumentService {

 @Override
 public List<Document> getAllDocuments(){
         //legacy code here as it is.
        // with no changes ...
  }
}

2. Tes unit kode warisan

Di sini kita memiliki kerja keras. Untuk mengatur test suite. Kita harus menetapkan sebanyak mungkin kasus: kasus sukses dan juga kasus kesalahan. Yang terakhir ini untuk kebaikan kualitas hasil.

Sekarang, alih-alih menguji MyDocumentService kita akan menggunakan antarmuka sebagai kontrak yang akan diuji.

Saya tidak akan masuk ke rincian, jadi maafkan saya Jika kode saya terlihat terlalu sederhana atau terlalu agnostik

public class DocumentServiceTestSuite {

   @Mock
   MyDependencyA mockDepA;

   @Mock
   MyDependencyB mockDepB;

    //... More mocks

   DocumentService service;

  @Before
   public void initService(){
       service = MyDocumentService(mockDepA, mockDepB);
      //this is purposed way to inject 
      //dependencies. Replace it with one you like more.  
   }

   @Test
   public void getAllDocumentsOK(){
         // here I mock depA and depB
         // wanted behaivors...

         List<Document> result = service.getAllDocuments();

          Assert.assertX(result);
          Assert.assertY(result);
           //... As many you think appropiate
    } 
 }

Tahap ini membutuhkan waktu lebih lama daripada yang lain dalam pendekatan ini. Dan itu yang paling penting karena akan menetapkan titik referensi untuk perbandingan di masa depan.

Catatan: Karena tidak ada perubahan besar yang dibuat dan behaivor tetap tidak tersentuh. Saya sarankan untuk melakukan tag di sini ke SCM. Tag atau cabang tidak masalah. Lakukan saja versi.

Kami menginginkannya untuk rollbacks, perbandingan versi dan mungkin untuk eksekusi paralel dari kode lama dan yang baru.

3. Refactoring

Refactor akan diimplementasikan menjadi komponen baru. Kami tidak akan melakukan perubahan pada kode yang ada. Langkah pertama semudah melakukan copy & paste MyDocumentService dan ganti namanya menjadi CustomDocumentService (misalnya).

Kelas baru terus menerapkan DocumentService . Lalu pergi dan refactorize getAllDocuments () . (Mari kita mulai dengan satu. Pin-refactor)

Mungkin memerlukan beberapa perubahan pada antarmuka / metode DAO. Jika demikian, jangan ubah kode yang ada. Terapkan metode Anda sendiri di antarmuka DAO. Kode lama Annotate sebagai Usang dan Anda akan tahu nanti apa yang harus dihapus.

Sangat penting untuk tidak merusak / mengubah implementasi yang ada. Kami ingin menjalankan kedua layanan secara paralel dan kemudian membandingkan hasilnya.

public class CustomDocumentService implements DocumentService {

 @Override
 public List<Document> getAllDocuments(){
         //new code here ...
         //due to im refactoring service 
         //I do the less changes possible on its dependencies (DAO).
         //these changes will come later 
         //and they will have their own tests
  }
 }

4. Memperbarui DocumentServiceTestSuite

Ok, sekarang bagian yang lebih mudah. Untuk menambah tes komponen baru.

public class DocumentServiceTestSuite {

   @Mock
   MyDependencyA mockDepA;

   @Mock
   MyDependencyB mockDepB;

   DocumentService service;
   DocumentService customService;

  @Before
   public void initService(){
       service = MyDocumentService(mockDepA, mockDepB);
        customService = CustomDocumentService(mockDepA, mockDepB);
       // this is purposed way to inject 
       //dependencies. Replace it with the one you like more
   }

   @Test
   public void getAllDocumentsOK(){
         // here I mock depA and depB
         // wanted behaivors...

         List<Document> oldResult = service.getAllDocuments();

          Assert.assertX(oldResult);
          Assert.assertY(oldResult);
           //... As many you think appropiate

          List<Document> newResult = customService.getAllDocuments();

          Assert.assertX(newResult);
          Assert.assertY(newResult);
           //... The very same made to oldResult

          //this is optional
Assert.assertEquals(oldResult,newResult);
    } 
 }

Sekarang kita memiliki oldResult dan newResult keduanya divalidasi secara independen tetapi kita juga dapat membandingkan satu sama lain. Validasi terakhir ini bersifat opsional dan tergantung pada hasilnya. Mungkin itu tidak sebanding.

Mungkin tidak membuat terlalu banyak seity untuk membandingkan dua koleksi dengan cara ini, tetapi akan berlaku untuk objek jenis lain (pojos, entitas model data, DTO, Pembungkus, tipe asli ...)

Catatan

Saya tidak akan berani memberi tahu bagaimana melakukan tes unit atau cara menggunakan lib mock. Saya tidak berani mengatakan bagaimana Anda harus melakukan refactor. Yang ingin saya lakukan adalah menyarankan strategi global. Cara membawanya tergantung pada Anda. Anda tahu persis bagaimana kode itu, kompleksitasnya dan apakah strategi seperti itu patut dicoba. Fakta seperti waktu dan sumber daya penting di sini. Juga penting apa yang Anda harapkan dari tes ini di masa depan.

Saya telah memulai contoh saya dengan Layanan dan saya akan mengikuti dengan DAO dan seterusnya. Masuk jauh ke tingkat ketergantungan. Kurang lebih bisa digambarkan sebagai strategi dari atas ke bawah . Namun untuk perubahan kecil / refaktor ( seperti yang terlihat pada contoh tur ), bottom up akan melakukan tugas dengan lebih mudah. Karena ruang lingkup perubahannya sedikit.

Akhirnya, terserah Anda untuk menghapus kode yang sudah usang dan untuk mengalihkan dependensi lama ke yang baru.

Hapus juga tes yang sudah usang dan pekerjaan sudah selesai. Jika Anda memberi versi solusi lama dengan pengujiannya, Anda dapat memeriksa dan membandingkan satu sama lain kapan saja.

Karena begitu banyak pekerjaan, Anda memiliki kode lama diuji, divalidasi, dan versi. Dan kode baru, diuji, divalidasi, dan siap diversi.


3

tl; dr Jangan menulis unit test. Tulis tes pada tingkat yang lebih tepat.


Mengingat definisi kerja Anda tentang refactoring:

Anda tidak mengubah apa yang dilakukan perangkat lunak Anda, Anda mengubah cara melakukannya

ada spektrum yang sangat luas. Di satu sisi adalah perubahan mandiri untuk metode tertentu, mungkin menggunakan algoritma yang lebih efisien. Di ujung lain porting ke bahasa lain.

Apa pun tingkat refactoring / desain ulang yang dilakukan, penting untuk memiliki tes yang beroperasi pada tingkat itu atau lebih tinggi.

Tes otomatis sering diklasifikasikan berdasarkan level sebagai:

  • Tes unit - Komponen individual (kelas, metode)

  • Tes integrasi - Interaksi antar komponen

  • Tes sistem - Aplikasi lengkap

Tulis tingkat tes yang dapat bertahan pada refactoring yang pada dasarnya tidak tersentuh.

Berpikir:

Apa yang penting, perilaku yang dapat dilihat secara publik yang akan dimiliki aplikasi sebelum dan sesudah refactoring? Bagaimana saya bisa menguji hal itu masih berfungsi sama?


2

Jangan buang waktu menulis tes yang menghubungkan pada titik-titik di mana Anda dapat mengantisipasi bahwa antarmuka akan berubah dengan cara yang tidak sepele. Ini sering merupakan tanda bahwa Anda mencoba untuk menguji unit-unit kelas yang sifatnya 'kolaboratif' - yang nilainya tidak dalam apa yang mereka lakukan sendiri, tetapi dalam cara mereka berinteraksi dengan sejumlah kelas yang terkait erat untuk menghasilkan perilaku yang berharga . Ini yang perilaku yang Anda ingin tes, yang berarti bahwa Anda ingin menguji pada tingkat yang lebih tinggi. Pengujian di bawah level ini seringkali membutuhkan banyak ejekan yang jelek, dan tes yang dihasilkan dapat menjadi hambatan bagi pengembangan daripada bantuan untuk mempertahankan perilaku.

Jangan terlalu terpaku pada apakah Anda membuat refactor, mendesain ulang, atau apa pun. Anda dapat membuat perubahan yang pada tingkat yang lebih rendah merupakan desain ulang sejumlah komponen, tetapi pada tingkat integrasi yang lebih tinggi hanya berarti sebuah refactor. Intinya adalah untuk menjadi jelas tentang perilaku apa yang bernilai bagi Anda, dan mempertahankan perilaku itu saat Anda pergi.

Mungkin bermanfaat untuk dipertimbangkan saat Anda menulis tes - bisakah saya dengan mudah menjelaskan kepada QA, pemilik produk, atau pengguna, tes apa yang sebenarnya diuji? Jika sepertinya menggambarkan tes akan terlalu esoteris dan teknis, mungkin Anda menguji pada tingkat yang salah. Tes pada poin / level yang 'masuk akal', dan jangan gabungkan kode Anda dengan tes di setiap level.


Selalu tertarik dengan alasan downvotes!
topo Reinstate Monica

1

Tugas pertama Anda adalah mencoba membuat "tanda tangan metode ideal" untuk pengujian Anda. Berusaha keras untuk menjadikannya fungsi murni . Ini harus independen dari kode yang sebenarnya sedang diuji; itu adalah lapisan adaptor kecil. Tulis kode Anda ke lapisan adaptor ini. Sekarang ketika Anda memperbaiki kode Anda, Anda hanya perlu mengubah lapisan adaptor. Ini adalah contoh sederhana:

[TestMethod]
public void simple_addition()
{
    Assert.AreEqual(7, Eval("3 + 4"));
}

[TestMethod]
public void order_of_operations()
{
    Assert.AreEqual(52, Eval("2 + 5 * 10"));
}

[TestMethod]
public void absolute_value()
{
    Assert.AreEqual(9, Eval("abs(-9)"));
    Assert.AreEqual(5, Eval("abs(5)"));
    Assert.AreEqual(0, Eval("abs(0)"));
}

static object Eval(string expression)
{
    // This is the code under test.
    // I can refactor this as much as I want without changing the tests.
    var settings = new EvaluatorSettings();
    Evaluator.Settings = settings;
    Evaluator.Evaluate(expression);
    return Evaluator.LastResult;
}

Tesnya bagus, tetapi kode yang diuji memiliki API yang buruk. Saya dapat melakukan refactor tanpa mengubah tes hanya dengan memperbarui lapisan adaptor saya:

static object Eval(string expression)
{
    // After refactoring...
    var settings = new EvaluatorSettings();
    var evaluator = new Evaluator(settings);
    return evaluator.Evaluate(expression);
}

Contoh ini tampaknya cukup jelas untuk dilakukan sesuai prinsip Jangan Ulangi Diri Sendiri, tetapi mungkin tidak begitu jelas dalam kasus lain. Keuntungan melampaui KERING - keuntungan sebenarnya adalah decoupling dari tes dari kode yang diuji.

Tentu saja, teknik ini mungkin tidak disarankan dalam semua situasi. Misalnya, tidak akan ada alasan untuk menulis adaptor untuk POCO / POJO karena mereka tidak benar-benar memiliki API yang dapat berubah secara independen dari kode pengujian. Juga jika Anda menulis sejumlah kecil tes, lapisan adaptor yang relatif besar mungkin akan sia-sia.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.