Apa yang terjadi dengan tes metode ketika metode itu menjadi pribadi setelah desain ulang di TDD?

29

Katakanlah saya mulai mengembangkan permainan peran dengan karakter yang menyerang karakter lain dan hal-hal semacam itu.

Menerapkan TDD, saya membuat beberapa test case untuk menguji logika di dalam Character.receiveAttack(Int)metode. Sesuatu seperti ini:

@Test
fun healthIsReducedWhenCharacterIsAttacked() {
    val c = Character(100) //arg is the health
    c.receiveAttack(50) //arg is the suffered attack damage
    assertThat(c.health, is(50));
}

Katakanlah saya punya 10 metode metode pengujian receiveAttack. Sekarang, saya menambahkan metode Character.attack(Character)(yang memanggil receiveAttackmetode), dan setelah beberapa siklus TDD mengujinya, saya membuat keputusan: Character.receiveAttack(Int)seharusnya private.

Apa yang terjadi dengan 10 kasus uji sebelumnya? Haruskah saya menghapusnya? Haruskah saya menjaga metode public(saya tidak berpikir begitu)?

Pertanyaan ini bukan tentang bagaimana menguji metode pribadi tetapi bagaimana menghadapinya setelah desain ulang ketika menerapkan TDD

Menggertak
sumber
2
Kemungkinan duplikat dari Metode pribadi yang dilindungi
agas
10
Jika bersifat pribadi Anda tidak mengujinya, itu mudah. Hapus dan lakukan tarian refactor
kayess
6
Saya mungkin menentang arus di sini. Tapi, saya biasanya menghindari metode pribadi dengan cara apa pun. Saya lebih suka tes lebih banyak daripada tes lebih sedikit. Saya tahu apa yang dipikirkan orang "Apa, jadi Anda tidak pernah memiliki fungsionalitas apa pun yang tidak ingin Anda paparkan kepada konsumen?". Ya, saya punya banyak yang tidak ingin saya ungkapkan. Alih-alih, ketika saya memiliki metode pribadi, saya malah mengubahnya menjadi kelas itu sendiri dan menggunakan kelas tersebut dari kelas aslinya. Kelas baru dapat ditandai sebagai internalatau setara dengan bahasa Anda untuk tetap mencegahnya diekspos. Sebenarnya jawaban Kevin Cline adalah pendekatan semacam ini.
user9993
3
@ user9993 Anda sepertinya memilikinya mundur. Jika penting bagi Anda untuk melakukan lebih banyak tes, satu-satunya cara untuk memastikan bahwa Anda tidak melewatkan sesuatu yang penting adalah dengan menjalankan analisis cakupan. Dan untuk alat peliputan tidak masalah sama sekali jika metode ini bersifat pribadi atau publik atau apa pun. Berharap bahwa membuat barang publik entah bagaimana akan mengimbangi kurangnya analisis cakupan memberikan rasa aman yang salah saya khawatir
Agak
2
@gnat Tapi saya tidak pernah mengatakan apa-apa tentang "tidak memiliki jangkauan"? Komentar saya tentang "Saya lebih suka tes lebih banyak daripada tes lebih sedikit" seharusnya membuat itu jelas. Tidak yakin apa yang Anda peroleh dengan tepat, tentu saja saya juga menguji kode yang saya ekstrak. Itulah intinya.
user9993

Jawaban:

52

Di TDD, tes berfungsi sebagai dokumentasi yang dapat dieksekusi dari desain Anda. Desain Anda berubah, jadi jelas, dokumentasi Anda juga harus!

Perhatikan bahwa, dalam TDD, satu-satunya cara attackmetode ini dapat muncul, adalah sebagai hasil dari gagal lulus ujian. Yang berarti, attacksedang diuji oleh beberapa tes lain. Yang berarti secara tidak langsung receiveAttack ditanggung oleh attacktes. Idealnya, setiap perubahan receiveAttackharus mematahkan setidaknya satu dari attacktes.

Dan jika tidak, maka ada fungsi receiveAttackyang tidak lagi diperlukan dan seharusnya tidak ada lagi!

Jadi, karena receiveAttacksudah diuji coba attack, tidak masalah apakah Anda menyimpan tes Anda atau tidak. Jika kerangka pengujian Anda membuatnya mudah untuk menguji metode pribadi, dan jika Anda memutuskan untuk menguji metode pribadi, maka Anda dapat menyimpannya. Tetapi Anda juga dapat menghapusnya tanpa kehilangan cakupan tes dan kepercayaan diri.

Jörg W Mittag
sumber
14
Ini adalah jawaban yang bagus, simpan untuk "Jika kerangka pengujian Anda membuatnya mudah untuk menguji metode pribadi, dan jika Anda memutuskan untuk menguji metode pribadi, maka Anda dapat menyimpannya." Metode pribadi adalah detail implementasi dan tidak boleh, pernah diuji secara langsung.
David Arno
20
@ Davidvido: Saya tidak setuju, dengan alasan itu internal modul tidak boleh diuji. Namun, internal modul dapat sangat kompleks dan karenanya memiliki unit-tes untuk setiap fungsi internal individu dapat berharga. Unit-tes digunakan untuk memeriksa invarian dari bagian fungsionalitas, jika metode privat memiliki invarian (pra-kondisi / pasca-kondisi) maka unit-test dapat bernilai.
Matthieu M.
8
" Dengan alasan itu internal modul tidak boleh diuji ". Internal itu seharusnya tidak pernah diuji secara langsung . Semua tes hanya boleh menguji API publik. Jika elemen internal tidak dapat dijangkau melalui API publik, maka hapus elemen itu karena tidak ada hasilnya.
David Arno
28
@ DavidVrno Dengan logika itu, jika Anda sedang membangun executable (bukan perpustakaan) maka Anda seharusnya tidak memiliki unit test sama sekali. - "Panggilan fungsi bukan bagian dari API publik! Hanya argumen baris perintah! Jika fungsi internal program Anda tidak dapat dijangkau melalui argumen baris perintah, maka hapus saja karena tidak ada artinya." - Sementara fungsi pribadi bukan bagian dari API publik kelas, mereka adalah bagian dari API internal kelas. Dan sementara Anda tidak perlu menguji API internal kelas, Anda bisa, menggunakan logika yang sama untuk menguji API internal yang dapat dieksekusi.
RM
7
@ RM, jika saya membuat executable dengan cara non-modular, maka saya akan dipaksa untuk memilih antara tes rapuh internal, atau hanya menggunakan tes integrasi menggunakan executable dan runtime I / O. Oleh karena itu dengan logika saya yang sebenarnya, daripada versi strawman Anda, saya akan membuatnya secara modular (misalnya melalui serangkaian perpustakaan). API publik dari modul-modul tersebut kemudian dapat diuji dengan cara yang tidak rapuh.
David Arno
23

Jika metode ini cukup kompleks sehingga perlu pengujian, itu harus umum di beberapa kelas. Jadi, Anda refactor dari:

public class X {
  private int complexity(...) {
    ...
  }
  public void somethingElse() {
    int c = complexity(...);
  }
}

untuk:

public class Complexity {
  public int calculate(...) {
    ...
  }
}

public class X {
  private Complexity complexity;
  public X(Complexity complexity) { // dependency injection happiness
    this.complexity = complexity;
  }

  public void something() {
    int c = complexity.calculate(...);
  }
}

Pindahkan tes saat ini untuk X.complexity ke ComplexityTest. Kemudian teks X.sesuatu dengan mengejek Kompleksitas.

Dalam pengalaman saya, refactoring menuju kelas yang lebih kecil dan metode yang lebih pendek memberikan manfaat besar. Mereka lebih mudah dipahami, lebih mudah untuk diuji, dan akhirnya digunakan kembali lebih dari yang mungkin diharapkan.

kevin cline
sumber
Jawaban Anda menjelaskan dengan lebih jelas gagasan yang saya coba jelaskan dalam komentar saya tentang pertanyaan OP. Jawaban bagus.
user9993
3
Terima kasih atas jawaban anda. Sebenarnya, metode acceptAttack cukup sederhana ( this.health = this.health - attackDamage). Mungkin mengekstraknya ke kelas lain adalah solusi overengineered, untuk saat ini.
Hector
1
Ini jelas merupakan cara yang berlebihan bagi OP - dia ingin pergi ke toko, bukan terbang ke bulan - tetapi solusi yang baik dalam kasus umum.
Jika fungsinya sesederhana itu mungkin terlalu banyak rekayasa bahwa itu bahkan didefinisikan sebagai fungsi di tempat pertama.
David K
1
mungkin berlebihan hari ini tetapi dalam waktu 6 bulan ketika ada satu ton perubahan pada kode ini manfaatnya akan jelas. Dan dalam setiap IDE yang layak hari ini pasti ekstraksi beberapa kode ke kelas yang terpisah harus beberapa penekanan tombol yang paling sulit solusi over-engineered mengingat bahwa dalam biner runtime semuanya akan mendidih ke yang sama pula.
Stephen Byrne
6

Katakanlah saya memiliki 10 metode pengujian metode acceptAttack. Sekarang, saya menambahkan metode Character.attack (Character) (yang memanggil metode acceptAttack), dan setelah beberapa siklus TDD mengujinya, saya membuat keputusan: Character.receiveAttack (Int) harus pribadi.

Yang perlu diingat di sini adalah bahwa keputusan yang Anda buat adalah menghapus metode dari API . Saran dari kompatibilitas mundur akan menyarankan

  1. Jika Anda tidak perlu menghapusnya, lalu tinggalkan di API
  2. Jika Anda tidak perlu menghapusnya belum , kemudian menandainya sebagai usang dan jika dokumen mungkin ketika akhir hidup akan terjadi
  3. Jika Anda perlu menghapusnya, maka Anda memiliki perubahan versi utama

Tes dihapus / atau diganti ketika API Anda tidak lagi mendukung metode ini. Pada titik itu, metode pribadi adalah detail implementasi yang Anda harus dapat refactor pergi.

Pada titik itu, Anda kembali ke pertanyaan standar tentang apakah rangkaian pengujian Anda harus secara langsung mengakses implementasi, lebih tepatnya berinteraksi murni melalui API publik. Metode pribadi adalah sesuatu yang harus bisa kita ganti tanpa test suite menghalangi . Jadi saya akan mengharapkan pasangan tes untuk pergi - baik mendapatkan pensiun, atau bergerak dengan implementasi ke komponen yang dapat diuji secara terpisah.

VoiceOfUnasonason
sumber
3
Penghentian tidak selalu menjadi perhatian. Dari pertanyaan: "Katakanlah saya mulai mengembangkan ..." jika perangkat lunak belum dirilis, penghentian adalah masalah non-. Selanjutnya: "permainan peran" menyiratkan ini bukan perpustakaan yang dapat digunakan kembali, tetapi perangkat lunak biner yang ditujukan untuk pengguna akhir. Meskipun beberapa perangkat lunak pengguna akhir memiliki API publik (mis. MS Office), sebagian besar tidak. Bahkan perangkat lunak yang memang memiliki API publik hanya memiliki sebagian saja terkena plugin, skrip (misalnya game dengan ekstensi LUA), atau fungsi lainnya. Namun, ada baiknya mengangkat ide untuk kasus umum yang dijelaskan OP.