Berjuang dengan ketergantungan siklus dalam unit test

24

Saya mencoba untuk mempraktekkan TDD, dengan menggunakannya untuk mengembangkan yang sederhana seperti Bit Vector. Saya kebetulan menggunakan Swift, tetapi ini adalah pertanyaan bahasa-agnostik.

My BitVectoradalah structyang menyimpan satu UInt64, dan menyajikan API di atasnya yang memungkinkan Anda memperlakukannya seperti koleksi. Detailnya tidak terlalu penting, tetapi cukup sederhana. 57 bit tinggi adalah bit penyimpanan, dan 6 bit lebih rendah adalah bit "hitung", yang memberitahu Anda berapa banyak bit penyimpanan yang benar-benar menyimpan nilai yang terkandung.

Sejauh ini, saya memiliki beberapa kemampuan yang sangat sederhana:

  1. Inisialisasi yang membangun vektor bit kosong
  2. Sebuah countproperti jenisInt
  3. Sebuah isEmptyproperti jenisBool
  4. Operator kesetaraan ( ==). NB: ini adalah operator kesetaraan nilai yang serupa dengan Object.equals()di Jawa, bukan operator kesetaraan referensi seperti ==di Jawa.

Saya mengalami sekelompok ketergantungan siklus:

  1. Tes unit yang menguji penginisialisasi saya perlu memverifikasi bahwa yang baru dibangun BitVector. Ini dapat dilakukan dengan salah satu dari 3 cara:

    1. Memeriksa bv.count == 0
    2. Memeriksa bv.isEmpty == true
    3. Periksa itu bv == knownEmptyBitVector

    Metode 1 mengandalkan count, metode 2 bergantung pada isEmpty(yang bergantung pada itu sendiri count, jadi tidak ada gunanya menggunakannya), metode 3 bergantung ==. Bagaimanapun, saya tidak dapat menguji inisialisasi saya secara terpisah.

  2. Tes countkebutuhan untuk beroperasi pada sesuatu, yang pasti menguji inisialisasi saya

  3. Implementasi isEmptymengandalkancount

  4. Implementasi ==mengandalkan count.

Saya dapat menyelesaikan sebagian masalah ini dengan memperkenalkan API pribadi yang membangun a BitVectordari pola bit yang ada (sebagai a UInt64). Ini memungkinkan saya untuk menginisialisasi nilai tanpa menguji inisialisasi lainnya, sehingga saya bisa "booting" dengan cara saya.

Agar unit test saya benar-benar menjadi unit test, saya menemukan diri saya melakukan banyak peretasan, yang menyulitkan prod dan kode pengujian saya secara substansial.

Bagaimana tepatnya Anda mengatasi masalah semacam ini?

Alexander - Pasang kembali Monica
sumber
20
Anda mengambil pandangan yang terlalu sempit pada istilah "unit". BitVectoradalah ukuran unit yang sangat baik untuk pengujian unit dan segera menyelesaikan masalah Anda bahwa anggota publik BitVectorsaling membutuhkan untuk melakukan tes yang bermakna.
Bart van Ingen Schenau
Anda tahu terlalu banyak detail implementasi di muka. Apakah pengembangan Anda benar-benar didorong oleh tes ?
herby
@herby Tidak, itu sebabnya saya berlatih. Meskipun itu tampak seperti standar yang benar-benar tidak mungkin tercapai. Saya tidak berpikir saya pernah memprogram apa pun tanpa perkiraan mental yang cukup jelas tentang apa yang akan terjadi pada implementasinya.
Alexander - Pasang kembali Monica
@Alexander Anda harus mencoba untuk rileks, jika tidak itu akan menjadi tes pertama, tetapi tidak didorong oleh tes. Katakan saja "Saya akan melakukan sedikit vektor dengan satu int 64bit sebagai backing store" dan hanya itu; sejak saat itu lakukan TDD red-green-refactor satu demi satu. Detail implementasi, serta API, harus muncul dari mencoba membuat tes berjalan (yang pertama), dan dari menulis tes tersebut di tempat pertama (yang terakhir).
herby

Jawaban:

66

Anda terlalu khawatir tentang detail implementasi.

Tidak masalah bahwa dalam implementasi Anda saat ini , isEmptybergantung pada count(atau hubungan apa pun yang mungkin Anda miliki): yang harus Anda perhatikan hanyalah antarmuka publik. Misalnya, Anda dapat melakukan tiga tes:

  • Bahwa objek yang baru diinisialisasi memiliki count == 0.
  • Bahwa objek yang baru diinisialisasi memiliki isEmpty == true
  • Bahwa objek yang baru diinisialisasi sama dengan objek kosong yang dikenal.

Ini semua adalah tes yang valid, dan menjadi sangat penting jika Anda pernah memutuskan untuk refactor internal kelas Anda sehingga isEmptymemiliki implementasi yang berbeda yang tidak bergantung count- selama tes Anda semua masih berlalu, Anda tahu Anda belum mundur apa pun.

Hal serupa berlaku untuk poin Anda yang lain - ingatlah untuk menguji antarmuka publik, bukan implementasi internal Anda. Anda mungkin menemukan TDD berguna di sini, karena Anda kemudian akan menulis tes yang Anda butuhkan isEmptysebelum Anda menulis implementasi apa pun untuk itu.

Philip Kendall
sumber
6
@Alexander Anda terdengar seperti pria yang membutuhkan definisi unit testing yang jelas. Yang terbaik yang saya tahu berasal dari Michael Feathers
candied_orange
14
@Alexander Anda memperlakukan setiap metode sebagai bagian kode yang dapat diuji secara independen. Itulah sumber kesulitan Anda. Kesulitan-kesulitan ini hilang jika Anda menguji objek secara keseluruhan, tanpa mencoba membaginya menjadi bagian-bagian yang lebih kecil. Ketergantungan antar objek tidak sebanding dengan ketergantungan antar metode.
Amon
9
@Alexander "sepotong kode" adalah pengukuran sewenang-wenang. Hanya dengan menginisialisasi variabel Anda menggunakan banyak "potongan kode". Yang penting adalah Anda menguji unit perilaku kohesif seperti yang didefinisikan oleh Anda .
Semut P
9
"Dari apa yang saya baca, saya mendapat kesan bahwa jika Anda hanya memecahkan sepotong kode, hanya unit test yang berhubungan langsung dengan kode itu yang harus gagal." Tampaknya itu aturan yang sangat sulit untuk diikuti. (mis. jika Anda menulis kelas vektor, dan Anda membuat kesalahan pada metode indeks, Anda mungkin akan memiliki banyak kerusakan di semua kode yang menggunakan kelas vektor itu)
jhominal
4
@Alexander Juga, lihat pola "Atur, Kerjakan, Tegas" untuk pengujian. Pada dasarnya, Anda mengatur objek dalam kondisi apa pun yang diperlukan (Atur), panggil metode yang benar-benar Anda uji (Bertindak) dan kemudian verifikasi bahwa statusnya berubah sesuai dengan harapan Anda. (Menegaskan). Hal-hal yang Anda atur di Arrange akan menjadi "prasyarat" untuk ujian.
GalacticCowboy
5

Bagaimana tepatnya Anda mengatasi masalah semacam ini?

Anda merevisi pemikiran Anda tentang apa itu "unit test".

Objek yang mengelola data yang dapat berubah dalam memori pada dasarnya adalah mesin negara. Jadi setiap use case yang berharga akan, setidaknya, memanggil metode untuk memasukkan informasi ke objek, dan memanggil metode untuk membaca salinan informasi dari objek. Dalam kasus penggunaan yang menarik, Anda juga akan menggunakan metode tambahan yang mengubah struktur data.

Dalam praktiknya, ini sering terlihat seperti

// GIVEN
obj = new Object(...)

// THEN
assert object.read(...)

atau

// GIVEN
obj = new Object(...)

// WHEN
object.change(...)

// THEN
assert object.read(...)

Terminologi "unit test" - yah, ia memiliki sejarah panjang yang tidak terlalu baik.

Saya menyebutnya unit test, tetapi mereka tidak cocok dengan definisi yang diterima dari unit test dengan baik - Kent Beck, Test Driven Development by Example

Kent menulis versi pertama SUnit pada tahun 1994 , pelabuhan ke JUnit adalah pada tahun 1998, draft pertama buku TDD adalah awal tahun 2002. Kebingungan memiliki banyak waktu untuk menyebar.

Gagasan kunci dari tes ini (lebih tepatnya disebut "tes programmer" atau "tes pengembang") adalah bahwa tes diisolasi satu sama lain. Tes tidak membagikan struktur data yang dapat berubah, sehingga dapat dijalankan secara bersamaan. Tidak ada kekhawatiran bahwa tes harus dijalankan dalam urutan tertentu untuk mengukur solusi dengan benar.

Kasus penggunaan utama untuk tes ini adalah bahwa tes tersebut dijalankan oleh programmer antara pengeditan dengan kode sumbernya sendiri. Jika Anda menjalankan protokol refactor merah hijau, RED yang tidak terduga selalu menunjukkan kesalahan dalam edit terakhir Anda; Anda mengembalikan perubahan itu, memverifikasi bahwa tesnya HIJAU, dan coba lagi. Tidak ada banyak keuntungan dalam mencoba berinvestasi dalam desain di mana setiap bug yang mungkin ditangkap hanya dengan satu tes.

Tentu saja, gabungan menggabungkan kesalahan, lalu menemukan bahwa kesalahan tidak lagi sepele. Ada berbagai langkah yang dapat Anda ambil untuk memastikan bahwa kesalahan mudah dilokalisasi. Lihat

VoiceOfUnreason
sumber
1

Secara umum (bahkan jika tidak menggunakan TDD) Anda harus berusaha keras untuk menulis tes sebanyak mungkin sambil pura-pura Anda tidak tahu bagaimana itu diterapkan.

Jika Anda benar-benar melakukan TDD itu sudah seharusnya demikian. Tes Anda adalah spesifikasi program yang dapat dieksekusi.

Bagaimana grafik panggilan terlihat di bawah tes tidak relevan, selama tes itu sendiri masuk akal dan terpelihara dengan baik.

Saya pikir masalah Anda adalah pemahaman Anda tentang TDD.

Masalah Anda menurut saya adalah bahwa Anda "mencampur" persona TDD Anda. Ide "test", "code", dan "refactor" Anda beroperasi sepenuhnya secara independen satu sama lain, idealnya. Khususnya, pengkodean dan refactoring personas Anda tidak memiliki kewajiban terhadap pengujian selain untuk membuat / membuatnya tetap hijau.

Tentu, pada prinsipnya, akan lebih baik jika semua tes ortogonal dan independen satu sama lain. Tapi itu bukan masalah dari dua persona TDD Anda yang lain, dan jelas bukan persyaratan keras yang ketat atau bahkan realistis dari tes Anda. Pada dasarnya: Jangan membuang akal sehat Anda tentang kualitas kode untuk mencoba memenuhi persyaratan yang tidak ada yang meminta Anda.

Tim Seguine
sumber