Cara memperbaiki kesalahan dalam ujian, setelah menulis implementasi

21

Apa tindakan terbaik dalam TDD jika, setelah menerapkan logika dengan benar, tes masih gagal (karena ada kesalahan dalam tes)?

Misalnya, Anda ingin mengembangkan fungsi berikut:

int add(int a, int b) {
    return a + b;
}

Misalkan kita mengembangkannya dalam langkah-langkah berikut:

  1. Tes tulis (belum berfungsi):

    // test1
    Assert.assertEquals(5, add(2, 3));
    

    Menghasilkan kesalahan kompilasi.

  2. Menulis implementasi fungsi dummy:

    int add(int a, int b) {
        return 5;
    }
    

    Hasil: test1melewati.

  3. Tambahkan test case lain:

    // test2 -- notice the wrong expected value (should be 11)!
    Assert.assertEquals(12, add(5, 6));
    

    Hasil: test2gagal, test1masih lewat.

  4. Tulis implementasi nyata:

    int add(int a, int b) {
        return a + b;
    }
    

    Hasil: test1masih lewat, test2masih gagal (sejak 11 != 12).

Dalam kasus khusus ini: apakah akan lebih baik untuk:

  1. memperbaiki test2, dan melihat bahwa itu sekarang lewat, atau
  2. hapus bagian implementasi yang baru (yaitu kembali ke langkah # 2 di atas), perbaiki test2dan biarkan gagal, dan kemudian masukkan kembali implementasi yang benar (langkah # 4. di atas).

Atau ada cara lain yang lebih pintar?

Sementara saya mengerti bahwa contoh masalah agak sepele, saya tertarik pada apa yang harus dilakukan dalam kasus generik, yang mungkin lebih kompleks daripada penambahan dua angka.

EDIT (Menanggapi jawaban dari @Thomas Junk):

Fokus pertanyaan ini adalah apa yang disarankan TDD dalam kasus seperti itu, bukan apa yang "praktik terbaik universal" untuk mencapai kode atau tes yang baik (yang mungkin berbeda dari cara TDD).

Attilio
sumber
3
Refactoring terhadap bilah merah adalah konsep yang relevan.
RubberDuck
5
Jelas, Anda harus melakukan TDD pada TDD Anda.
Blrfl
17
Jika ada yang bertanya mengapa saya skeptis terhadap TDD, saya akan mengarahkan mereka ke pertanyaan ini. Ini adalah Kafkaesque.
Traubenfuchs
@ Bllfl itulah yang dikatakan Xibit kepada kami »Saya meletakkan TDD di TDD sehingga Anda dapat TDD sementara TDDing«: D
Thomas Junk
3
@ Traubenfuchs saya akui pertanyaannya tampak konyol pada pandangan pertama dan saya bukan penganjur "do TDD sepanjang waktu", tapi saya yakin ada manfaat yang kuat untuk melihat tes gagal, lalu menulis kode yang membuat tes lulus (yang sebenarnya adalah pertanyaan ini sebenarnya).
Vincent Savard

Jawaban:

31

Yang benar-benar penting adalah bahwa Anda melihat tes lulus dan gagal.

Apakah Anda menghapus kode untuk membuat tes gagal maka menulis ulang kode atau menyelinap ke clipboard hanya untuk menempelkannya kembali nanti tidak masalah. TDD tidak pernah mengatakan Anda harus mengetik ulang apa pun. Ia ingin mengetahui tes lulus hanya ketika harus lulus dan gagal hanya ketika seharusnya gagal.

Melihat tes lulus dan gagal adalah cara Anda menguji tes. Jangan pernah percaya pada tes yang belum pernah Anda lihat lakukan keduanya.


Refactoring Against The Red Bar memberi kita langkah formal untuk refactoring tes kerja:

  • Jalankan tes
    • Perhatikan bilah hijau
    • Hancurkan kode yang sedang diuji
  • Jalankan tes
    • Perhatikan bilah merah
    • Perbaiki tes
  • Jalankan tes
    • Perhatikan bilah merah
    • Lepaskan kode yang sedang diuji
  • Jalankan tes
    • Perhatikan bilah hijau

Namun, kami tidak refactoring tes kerja. Kita harus mengubah tes kereta. Satu masalah adalah kode yang diperkenalkan sementara hanya tes ini yang membahasnya. Kode tersebut harus diputar kembali dan diperkenalkan kembali setelah tes diperbaiki.

Jika bukan itu masalahnya, dan cakupan kode bukan masalah karena tes lain yang mencakup kode, Anda dapat mengubah tes dan memperkenalkannya sebagai tes hijau.

Di sini, kode juga sedang diputar kembali tetapi cukup untuk menyebabkan tes gagal. Jika itu tidak cukup untuk mencakup semua kode yang diperkenalkan sementara hanya tercakup oleh tes kereta kami perlu kode yang lebih besar memutar kembali dan lebih banyak tes.

Memperkenalkan tes hijau

  • Jalankan tes
    • Perhatikan bilah hijau
    • Hancurkan kode yang sedang diuji
  • Jalankan tes
    • Perhatikan bilah merah
    • Lepaskan kode yang sedang diuji
  • Jalankan tes
    • Perhatikan bilah hijau

Memecah kode dapat mengomentari kode atau memindahkannya ke tempat lain hanya untuk menempelkannya kembali nanti. Ini menunjukkan kepada kita lingkup kode yang dicakup tes.

Untuk dua putaran terakhir ini, Anda kembali ke siklus hijau merah normal. Anda hanya menempel bukannya mengetik untuk membatalkan kode dan membuat lulus ujian. Jadi pastikan Anda hanya menyisipkan cukup untuk lulus ujian.

Pola keseluruhan di sini adalah untuk melihat warna tes mengubah cara yang kita harapkan. Perhatikan bahwa ini menciptakan situasi di mana Anda secara singkat memiliki tes hijau yang tidak dipercaya. Berhati-hatilah agar tidak terganggu dan lupa di mana Anda berada dalam langkah-langkah ini.

Terima kasih saya kepada RubberDuck untuk tautan Embracing the Red Bar .

candied_orange
sumber
2
Saya paling suka jawaban ini: Sangat penting untuk melihat tes gagal dengan kode yang salah, jadi saya akan menghapus / berkomentar kode, memperbaiki tes dan melihat mereka gagal, memasukkan kembali kode (mungkin memperkenalkan kesalahan yang disengaja untuk menempatkan tes ke tes) dan perbaiki kode untuk membuatnya bekerja. Sangat XP untuk sepenuhnya menghapus dan menulis ulang, tetapi kadang-kadang Anda hanya perlu pragmatis. ;)
GolezTrol
@ GolezTrol Saya pikir jawaban saya mengatakan hal yang sama, jadi saya menghargai umpan balik yang Anda miliki tentang apakah itu tidak jelas.
jonrsharpe
@jonrsharpe Jawaban Anda juga bagus, dan saya membatalkannya sebelum membaca yang ini. Tetapi di mana Anda sangat ketat dalam mengembalikan kode, CandiedOrange menyarankan pendekatan yang lebih pragmatis yang lebih menarik bagi saya.
GolezTrol
@ GolezTrol Saya tidak mengatakan cara mengembalikan kode; beri komentar, potong dan tempel, simpan, gunakan riwayat IDE Anda; itu tidak masalah. Yang penting adalah mengapa Anda melakukannya: sehingga Anda dapat memeriksa bahwa Anda mendapatkan kegagalan yang tepat . Saya sudah mengedit, semoga klarifikasi.
jonrsharpe
10

Apa tujuan keseluruhan yang ingin Anda capai?

  • Membuat tes yang bagus?

  • Membuat implementasi yang benar ?

  • Melakukan TTD dengan benar secara agama ?

  • Bukan dari salah satu di atas?

Mungkin Anda terlalu memikirkan hubungan Anda dengan tes dan pengujian.

Tes tidak memberikan jaminan tentang kebenaran implementasi. Setelah semua tes lulus tidak mengatakan apa-apa, apakah perangkat lunak Anda melakukan apa yang seharusnya; itu tidak membuat pernyataan penting tentang perangkat lunak Anda.

Ambil contoh Anda:

Implementasi "benar" penambahan akan menjadi kode yang setara dengan a+b. Dan selama kode Anda melakukan itu, Anda akan mengatakan algoritma itu benar dalam apa yang dilakukannya dan diimplementasikan dengan benar .

int add(int a, int b) {
    return a + b;
}

Pada pandangan pertama , kami berdua akan setuju, bahwa ini adalah implementasi dari penambahan.

Tapi apa yang kita lakukan benar-benar tidak mengatakan, bahwa kode ini adalah pelaksanaan additionitu hanya berperilaku untuk tingkat tertentu seperti: memikirkan bilangan bulat melimpah .

Integer overflow memang terjadi dalam kode, tetapi tidak dalam konsep addition. Jadi: kode Anda berlaku sampai batas tertentu seperti konsep addition, tetapi tidak addition.

Sudut pandang yang agak filosofis ini memiliki beberapa konsekuensi.

Dan satu, yang bisa Anda katakan, tes tidak lebih dari asumsi perilaku yang diharapkan dari kode Anda. Dalam pengujian kode Anda, Anda bisa (mungkin) tidak pernah pastikan, Anda pelaksanaannya adalah benar , yang terbaik Anda bisa katakan adalah, bahwa harapan Anda pada apa hasil kode Anda delivers berada atau tidak dipenuhi; baik itu, bahwa kode Anda salah, baik itu, bahwa tes Anda salah atau baik itu, bahwa keduanya salah.

Tes yang berguna membantu Anda untuk memperbaiki harapan Anda pada apa yang harus dilakukan kode: selama saya tidak mengubah harapan saya dan selama kode yang dimodifikasi memberi saya hasil yang saya harapkan, saya bisa yakin, bahwa asumsi yang saya buat tentang hasilnya sepertinya berhasil.

Itu tidak membantu, ketika Anda membuat asumsi yang salah; tapi hey! setidaknya itu mencegah skizofrenia: mengharapkan hasil yang berbeda ketika seharusnya tidak ada.


tl; dr

Apa tindakan terbaik dalam TDD jika, setelah menerapkan logika dengan benar, tes masih gagal (karena ada kesalahan dalam tes)?

Tes Anda adalah asumsi tentang perilaku kode. Jika Anda memiliki alasan kuat untuk menganggap implementasi Anda benar, perbaiki tes dan lihat apakah asumsi itu berlaku.

Thomas Junk
sumber
1
Saya pikir pertanyaan tentang tujuan keseluruhan cukup penting, terima kasih telah membawanya. Bagi saya, prio tertinggi adalah sebagai berikut: 1. implementasi yang benar 2. tes "bagus" (atau, saya lebih suka mengatakan, tes "berguna" / "dirancang dengan baik"). Saya melihat TDD sebagai alat yang mungkin untuk mencapai dua tujuan tersebut. Jadi, sementara saya tidak ingin secara agama mengikuti TDD, dalam konteks pertanyaan ini, saya lebih tertarik pada perspektif TDD. Saya akan mengedit pertanyaan untuk memperjelas ini.
Attilio
Jadi, akankah Anda menulis tes yang menguji untuk overflow dan lulus ketika itu terjadi atau apakah Anda membuatnya gagal ketika itu terjadi karena algoritma adalah penjumlahan dan melimpah menghasilkan jawaban yang salah?
Jerry Jeremiah
1
@JerryJeremiah Maksud saya adalah: Apa yang harus Anda uji tergantung pada penggunaan Anda. Untuk kasus penggunaan di mana Anda menambahkan sekelompok digit tunggal, algoritme cukup baik . Jika Anda tahu, bahwa kemungkinan besar Anda menambahkan "angka besar", datatypeitu jelas merupakan pilihan yang salah. Sebuah tes akan mengungkapkan bahwa: harapan Anda akan »bekerja untuk angka besar« dan dalam beberapa kasus tidak terpenuhi. Maka pertanyaannya adalah bagaimana menangani kasus-kasus itu. Apakah mereka kasus sudut? Ketika ya, bagaimana cara menghadapinya? Mungkin beberapa klausa quard membantu mencegah kekacauan yang lebih besar. Jawabannya adalah konteks terikat.
Thomas Junk
7

Anda perlu tahu bahwa tes ini akan gagal jika implementasi salah, yang tidak sama dengan lulus jika pelaksanaannya benar. Oleh karena itu Anda harus meletakkan kode kembali ke kondisi di mana Anda mengharapkannya gagal sebelum memperbaiki tes, dan pastikan gagal karena alasan yang Anda harapkan (yaitu 5 != 12), daripada sesuatu yang tidak Anda prediksi.

Jonrsharpe
sumber
Bagaimana kita dapat memeriksa bahwa tes gagal karena alasan yang kita harapkan?
Basilevs
2
@Basilevs Anda: 1. membuat hipotesis tentang apa alasan kegagalan seharusnya; 2. jalankan tes; dan 3. membaca pesan kegagalan yang dihasilkan dan membandingkan. Terkadang ini juga menyarankan cara Anda dapat menulis ulang tes untuk memberi Anda kesalahan yang lebih bermakna (misalnya, assertTrue(5 == add(2, 3))memberikan hasil yang kurang berguna daripada assertEqual(5, add(2, 3))meskipun keduanya menguji hal yang sama).
jonrsharpe
Masih belum jelas bagaimana menerapkan prinsip ini di sini. Saya memiliki hipotesis - tes mengembalikan nilai konstan, bagaimana menjalankan tes yang sama lagi akan memastikan saya benar? Jelas untuk menguji itu, saya perlu tes LAIN. Saya menyarankan untuk menambahkan contoh eksplisit untuk menjawab.
Basilevs
1
@ Basilevs apa? Hipotesis Anda di sini pada langkah 3 adalah "tes gagal karena 5 tidak sama dengan 12" . Menjalankan tes akan menunjukkan kepada Anda apakah tes gagal karena alasan itu, dalam hal ini Anda melanjutkan, atau karena alasan lain, dalam hal ini Anda mengetahui alasannya. Mungkin ini masalah bahasa, tapi tidak jelas bagi saya apa yang Anda sarankan.
jonrsharpe
5

Dalam kasus khusus ini, jika Anda mengubah 12 ke 11, dan tes sekarang berlalu, saya pikir Anda telah melakukan pekerjaan yang baik untuk menguji tes serta implementasinya, jadi tidak banyak yang perlu melalui rintangan tambahan.

Namun, masalah yang sama dapat muncul dalam situasi yang lebih kompleks, seperti ketika Anda memiliki kesalahan dalam kode pengaturan Anda. Dalam hal ini, setelah memperbaiki tes Anda, Anda mungkin harus mencoba memutasikan implementasi Anda sedemikian rupa agar tes tertentu gagal, dan kemudian mengembalikan mutasi. Jika mengembalikan implementasi adalah cara termudah untuk melakukannya, maka itu tidak masalah. Dalam contoh Anda, Anda mungkin bermutasi a + bke a + aatau a * b.

Atau, jika Anda dapat sedikit mengubah pernyataan dan melihat tes gagal, itu bisa sangat efektif untuk menguji tes.

Vaughn Cato
sumber
0

Saya akan mengatakan, ini adalah kasus untuk sistem kontrol versi favorit Anda:

  1. Tahap koreksi tes, menjaga perubahan kode Anda di direktori kerja Anda.
    Komit dengan pesan yang sesuai Fixed test ... to expect correct output.

    Dengan git, ini mungkin memerlukan penggunaan git add -pjika pengujian dan implementasi berada dalam file yang sama, jika tidak, Anda dapat dengan mudah mem-stage kedua file secara terpisah.

  2. Komit kode implementasi.

  3. Kembali ke masa lalu untuk menguji komit yang dilakukan pada langkah 1, memastikan bahwa tes benar-benar gagal .

Anda lihat, dengan begitu Anda tidak bergantung pada kecakapan mengedit Anda untuk memindahkan kode implementasi Anda keluar dari jalan saat Anda menguji tes gagal Anda. Anda menggunakan VCS Anda untuk menyimpan pekerjaan Anda dan untuk memastikan bahwa riwayat VCS yang tercatat dengan benar mencakup tes gagal dan lulus.

cmaster - mengembalikan monica
sumber