Bagaimana saya menguji sistem di mana benda-benda sulit untuk diejek?

34

Saya bekerja dengan sistem berikut:

Network Data Feed -> Third Party Nio Library -> My Objects via adapter pattern

Kami baru-baru ini memiliki masalah di mana saya memperbarui versi perpustakaan yang saya gunakan, yang, antara lain, menyebabkan cap waktu (yang dikembalikan oleh perpustakaan pihak ketiga long), diubah dari milidetik setelah zaman ke zaman nanodetik setelah zaman.

Masalah:

Jika saya menulis tes yang mengejek objek perpustakaan pihak ketiga, tes saya akan salah jika saya telah membuat kesalahan tentang objek perpustakaan pihak ketiga. Sebagai contoh, saya tidak menyadari bahwa stempel waktu berubah presisi, yang mengakibatkan perlunya perubahan dalam tes unit, karena tiruan saya mengembalikan data yang salah. Ini bukan bug di perpustakaan , itu terjadi karena saya melewatkan sesuatu di dokumentasi.

Masalahnya adalah, saya tidak bisa yakin tentang data yang terkandung dalam struktur data ini karena saya tidak bisa menghasilkan yang nyata tanpa umpan data nyata. Objek-objek ini besar dan rumit dan memiliki banyak potongan data yang berbeda di dalamnya. Dokumentasi untuk perpustakaan pihak ketiga buruk.

Pertanyaan:

Bagaimana saya bisa mengatur tes saya untuk menguji perilaku ini? Saya tidak yakin dapat menyelesaikan masalah ini dalam unit test, karena tes itu sendiri dapat dengan mudah salah. Selain itu, sistem terintegrasinya besar dan rumit dan mudah untuk melewatkan sesuatu. Misalnya, dalam situasi di atas, saya telah menyesuaikan penanganan stempel waktu dengan benar di beberapa tempat, tetapi saya melewatkan satu di antaranya. Sistem tampaknya melakukan sebagian besar hal yang benar dalam pengujian integrasi saya, tetapi ketika saya menggunakannya untuk produksi (yang memiliki lebih banyak data), masalahnya menjadi jelas.

Saya tidak memiliki proses untuk tes integrasi saya sekarang. Pengujian pada dasarnya adalah: cobalah untuk menjaga unit tes tetap bagus, tambahkan lebih banyak tes saat ada masalah, kemudian gunakan ke server pengujian saya dan pastikan semuanya tampak waras, kemudian gunakan untuk produksi. Masalah stempel waktu ini lulus uji unit karena tiruan dibuat salah, lalu lulus uji integrasi karena tidak menyebabkan masalah langsung yang jelas. Saya tidak punya departemen QA.

durron597
sumber
3
Bisakah Anda "merekam" data nyata dan "memutarnya" nanti di perpustakaan pihak ketiga?
Idan Arye
2
Seseorang bisa menulis buku tentang masalah seperti ini. Bahkan, Michael Feathers memang menulis buku itu saja: c2.com/cgi/wiki?WorkingEffectivelyWithLegacyCode Di dalamnya, ia menjelaskan sejumlah teknik untuk memecahkan dependensi yang sulit sehingga kode dapat menjadi lebih dapat diuji.
cbojar
2
Adaptor di sekitar perpustakaan pihak ketiga? Ya, itulah yang saya rekomendasikan. Tes unit tersebut tidak akan meningkatkan kode Anda. Mereka tidak akan membuatnya lebih dapat diandalkan atau lebih dapat dipelihara. Anda hanya menduplikasi sebagian kode orang lain pada saat itu; dalam hal ini, Anda menduplikasi beberapa kode yang ditulis dengan buruk dari suara itu. Itu rugi bersih. Beberapa jawaban menyarankan melakukan beberapa pengujian integrasi; itu ide yang bagus jika Anda hanya menginginkan, "Apakah ini berhasil?" cek kewarasan. Pengujian yang baik sulit, dan dibutuhkan keterampilan dan intuisi yang sama banyaknya dengan kode yang baik.
jpmc26
4
Ilustrasi sempurna kejahatan bawaan. Mengapa tidak perpustakaan mengembalikan Timestampkelas (mengandung representasi yang mereka inginkan) dan menyediakan metode bernama ( .seconds(), .milliseconds(), .microseconds(), .nanoseconds()) dan tentu saja bernama konstruktor. Maka tidak akan ada masalah.
Matthieu M.
2
Pepatah "semua masalah dalam pengkodean dapat diselesaikan dengan lapisan tipuan (kecuali, tentu saja, masalah terlalu banyak lapisan tipuan)" terlintas dalam pikiran di sini ..
Dan Pantry

Jawaban:

27

Sepertinya Anda sudah melakukan uji tuntas. Tapi ...

Pada tingkat yang paling praktis, selalu sertakan beberapa tes integrasi "loop penuh" yang baik dalam kode Anda untuk kode Anda sendiri, dan tulis lebih banyak pernyataan daripada yang Anda pikir Anda butuhkan. Khususnya, Anda harus memiliki beberapa tes yang melakukan siklus validasi create-read- [do_stuff] penuh.

[TestMethod]
public void MyFormatter_FormatsTimesCorrectly() {

  // this test isn't necessarily about the stream or the external interpreter.
  // but ... we depend on them working how we think they work:
  var stream = new StreamThingy();
  var interpreter = new InterpreterThingy(stream);
  stream.Write("id-123, some description, 12345");

  // this is what you're actually testing. but, it'll also hiccup
  // if your 3rd party dependencies introduce a breaking change.
  var formatter = new MyFormatter(interpreter);
  var line = formatter.getLine();
  Assert.equal(
    "some description took 123.45 seconds to complete (id-123)", line
  );
}

Dan sepertinya Anda sudah melakukan hal semacam ini. Anda hanya berurusan dengan perpustakaan yang rapuh dan / atau rumit. Dan dalam hal ini, ada baiknya untuk melakukan beberapa jenis tes "ini adalah cara perpustakaan bekerja" yang keduanya memverifikasi pemahaman Anda tentang perpustakaan dan berfungsi sebagai contoh cara menggunakan perpustakaan.

Misalkan Anda perlu memahami dan bergantung pada bagaimana parser JSON menginterpretasikan setiap "tipe" dalam string JSON. Sangat membantu dan sepele untuk memasukkan sesuatu seperti ini di suite Anda:

[TestMethod]
public void JSONParser_InterpretsTypesAsExpected() {
  String datastream = "{nbr:11,str:"22",nll:null,udf:undefined}";
  var o = (new JSONParser()).parse(datastream);

  Assert.equal(11, o.nbr);
  Assert.equal(Int32.getType(), o.nbr.getType());
  Assert.equal("22", o.str);
  Assert.equal(null, o.nll);
  Assert.equal(Object.getType(), o.nll.getType());
  Assert.isFalse(o.KeyExists(udf));
}

Tetapi kedua, ingat bahwa pengujian otomatis dalam bentuk apa pun, dan pada hampir semua tingkat kekakuan, masih akan gagal melindungi Anda dari semua bug. Sangat umum untuk menambahkan tes saat Anda menemukan masalah. Tidak memiliki departemen QA, ini berarti banyak masalah yang akan ditemukan oleh pengguna akhir.

Dan pada tingkat yang signifikan, itu wajar saja.

Dan ketiga, ketika pustaka mengubah arti nilai-kembali atau bidang tanpa mengganti nama bidang atau metode atau sebaliknya "melanggar" kode dependen (mungkin dengan mengubah tipenya), saya akan sangat tidak senang dengan penerbit itu. Dan saya berpendapat bahwa, meskipun Anda mungkin harus membaca changelog jika ada, Anda mungkin juga harus menyerahkan sebagian stres Anda kepada penerbit. Saya berpendapat mereka membutuhkan kritik yang diharapkan-konstruktif ...

svidgen
sumber
Ugh, saya berharap itu semudah memberi makan string json ke perpustakaan. Ini bukan. Saya tidak bisa melakukan yang setara (new JSONParser()).parse(datastream), karena mereka mengambil data langsung dari a NetworkInterfacedan semua kelas yang melakukan parsing sebenarnya adalah paket pribadi dan proguarded.
durron597
Juga, changelog tidak menyertakan fakta bahwa mereka mengubah cap waktu dari ms ke ns, di antara sakit kepala lainnya yang tidak mereka dokumentasikan. Ya, saya sangat tidak senang dengan mereka, dan saya telah menyatakan ini kepada mereka.
durron597
@ durron597 Oh, hampir tidak pernah. Tapi, Anda bisa sering memalsukan sumber data yang mendasarinya - seperti pada contoh kode pertama. ... Intinya adalah: lakukan tes integrasi loop penuh bila memungkinkan, uji pemahaman Anda tentang perpustakaan jika memungkinkan, dan hanya perlu diketahui bahwa Anda masih akan membiarkan bug masuk ke alam liar. Dan vendor pihak ketiga Anda harus bertanggung jawab untuk membuat perubahan yang tidak terlihat dan melanggar.
svidgen
@ durron597 Saya tidak terbiasa dengan NetworkInterface... apakah ini sesuatu yang Anda dapat memasukkan data ke dengan menghubungkan antarmuka ke port di localhost atau sesuatu?
svidgen
NetworkInterface. Ini adalah objek tingkat rendah untuk langsung bekerja dengan kartu jaringan dan membuka soket di atasnya, dll.
durron597
11

Jawaban singkat: Sulit. Anda mungkin merasa tidak ada jawaban yang baik, dan itu karena tidak ada jawaban yang mudah.

Jawaban panjang: Seperti yang dikatakan @ptyx , Anda memerlukan tes sistem dan tes integrasi serta tes unit:

  • Tes unit cepat dan mudah dijalankan. Mereka menangkap bug di setiap bagian kode dan menggunakan tiruan untuk membuatnya berjalan. Karena kebutuhan, mereka tidak dapat menangkap ketidakcocokan antara potongan kode (seperti milidetik versus nanodetik).
  • Tes integrasi dan pengujian sistem lambat (er) dan sulit (er) untuk dijalankan tetapi menangkap lebih banyak kesalahan.

Beberapa saran spesifik:

  • Ada beberapa manfaat dengan hanya mendapatkan tes sistem untuk menjalankan sistem sebanyak mungkin. Bahkan jika itu tidak dapat memvalidasi banyak perilaku atau melakukannya dengan sangat baik dalam menentukan masalah. (Micheal Feathers membahas ini lebih dalam Bekerja Efektif dengan Legacy Code .)
  • Berinvestasi dalam testabilitas membantu. Ada sejumlah besar teknik yang dapat Anda gunakan di sini: integrasi berkelanjutan, skrip, VM, alat untuk memutar ulang, proksi, atau mengarahkan lalu lintas jaringan.
  • Salah satu keuntungan (bagi saya, setidaknya) dari berinvestasi dalam testability mungkin tidak jelas: jika tes itu membosankan, menjengkelkan, atau rumit untuk menulis atau menjalankan, maka terlalu mudah bagi saya untuk hanya melewatkannya jika saya ditekan atau lelah. Menjaga tes Anda di bawah ambang batas "Sangat mudah sehingga tidak ada alasan untuk tidak melakukan ini" adalah penting.
  • Perangkat lunak yang sempurna tidak layak. Seperti yang lainnya, upaya yang dihabiskan untuk pengujian adalah suatu tradeoff, dan kadang-kadang itu tidak sepadan dengan usaha. Kendala (seperti kurangnya departemen QA Anda) ada. Terima bahwa bug akan terjadi, pulih, dan belajar.

Saya telah melihat pemrograman yang digambarkan sebagai aktivitas belajar tentang masalah dan ruang solusi. Mendapatkan segala sesuatunya sebelum waktunya mungkin tidak layak, tetapi Anda bisa belajar setelah itu. ("Saya memperbaiki penanganan stempel waktu di beberapa tempat tetapi terlewatkan satu. Dapatkah saya mengubah tipe data atau kelas saya untuk membuat penanganan stempel waktu lebih eksplisit dan lebih sulit untuk dilewatkan, atau membuatnya lebih tersentralisasi sehingga saya hanya memiliki satu tempat untuk berubah? Dapatkah saya memodifikasi pengujian saya untuk memverifikasi lebih banyak aspek dari penanganan stempel waktu? Dapatkah saya menyederhanakan lingkungan pengujian saya untuk membuat ini lebih mudah di masa depan? Dapatkah saya membayangkan beberapa alat yang akan membuat ini lebih mudah, dan jika demikian, dapatkah saya menemukan alat seperti itu di Google? "Dll)

Josh Kelley
sumber
7

Saya memperbarui versi perpustakaan ... yang ... menyebabkan stempel waktu (yang dikembalikan oleh perpustakaan pihak ketiga long), diubah dari milidetik setelah zaman ke nanodetik setelah zaman.

...

Ini bukan bug di perpustakaan

Saya sangat tidak setuju dengan Anda di sini. Itu adalah bug di perpustakaan , sebenarnya agak berbahaya. Mereka telah mengubah tipe semantik dari nilai balik, tetapi tidak mengubah jenis program dari nilai balik. Ini dapat mendatangkan semua jenis malapetaka, terutama jika ini adalah versi kecil, tetapi juga jika itu adalah versi utama.

Katakanlah sebaliknya perpustakaan mengembalikan jenis MillisecondsSinceEpoch, pembungkus sederhana yang menampung a long. Ketika mereka mengubahnya ke NanosecondsSinceEpochnilai, kode Anda akan gagal dikompilasi, dan jelas akan mengarahkan Anda ke tempat-tempat di mana Anda perlu melakukan perubahan. Perubahan tidak bisa merusak program Anda secara diam-diam.

Lebih baik lagi akan menjadi TimeSinceEpochobjek yang dapat mengadaptasi antarmuka itu sebagai lebih presisi ditambahkan, seperti menambahkan #toLongNanosecondsmetode di samping #toLongMillisecondsmetode, tidak memerlukan perubahan pada kode Anda sama sekali.

Masalah berikutnya adalah bahwa Anda tidak memiliki satu set tes integrasi yang dapat diandalkan ke perpustakaan. Anda harus menulis itu. Lebih baik membuat antarmuka di perpustakaan itu untuk merangkumnya dari sisa aplikasi Anda. Beberapa jawaban lain membahas hal ini (dan banyak lagi yang terus bermunculan saat saya mengetik). Tes integrasi harus dijalankan lebih jarang daripada tes unit Anda. Itu sebabnya memiliki lapisan penyangga membantu. Pisahkan tes integrasi Anda ke area yang terpisah (atau beri nama berbeda) sehingga Anda dapat menjalankannya sesuai kebutuhan, tetapi tidak setiap kali Anda menjalankan tes unit Anda.

cbojar
sumber
2
@ durron597 Saya masih berpendapat bahwa ini adalah bug. Di luar kekurangan dokumentasi, mengapa mengubah perilaku yang diharapkan sama sekali? Mengapa tidak menggunakan metode baru yang memberikan ketepatan baru, dan membiarkan metode lama tetap menyediakan milis? Dan mengapa tidak memberikan cara bagi kompiler untuk mengingatkan Anda melalui perubahan tipe pengembalian? Tidak perlu banyak untuk membuatnya lebih jelas, tidak hanya dalam dokumentasi, tetapi dalam kode itu sendiri.
cbojar
1
@ gbjbaanb, "bahwa mereka memiliki praktik rilis yang buruk" tampaknya seperti bug bagi saya
Arturo Torres Sánchez
2
@ gbjbaanb Perpustakaan pihak ke-3 [harus] membuat "kontrak" dengan para penggunanya. Melanggar kontrak itu - apakah itu didokumentasikan atau tidak - dapat / harus dianggap sebagai bug. Seperti yang orang lain katakan, jika Anda harus mengubah sesuatu, tambahkan ke kontrak dengan fungsi / metode baru (lih. Semua ...Ex()metode di Win32API). Jika ini tidak layak, "melanggar" kontrak dengan mengganti nama fungsi (atau jenis kembalinya) akan lebih baik daripada mengubah perilaku.
TripeHound
1
Ini adalah bug di perpustakaan. Menggunakan nanodetik dalam waktu lama mendorongnya.
Joshua
1
@ gbjbaanb Anda mengatakan ini bukan bug karena perilaku yang dimaksudkan, meskipun tidak terduga. Dalam hal ini itu bukan bug implementasi , tetapi itu adalah bug yang sama. Ini bisa disebut cacat desain atau bug penghubung . Kesalahannya terletak pada fakta bahwa ia mengekspos obsesi primitif dengan unit rindu daripada eksplisit, abstraksinya bocor karena mengekspor rincian implementasi internal (bahwa data disimpan sebagai panjang unit tertentu), dan melanggar prinsip tercengang dengan perubahan unit yang halus.
cbojar
5

Anda memerlukan tes integrasi dan sistem.

Tes unit sangat bagus untuk memverifikasi bahwa kode Anda berperilaku seperti yang Anda harapkan. Ketika Anda menyadari, itu tidak melakukan apa pun untuk menantang asumsi Anda atau memastikan harapan Anda waras.

Kecuali jika produk Anda hanya memiliki sedikit interaksi dengan sistem eksternal, atau berinteraksi dengan sistem yang sangat terkenal, stabil, dan terdokumentasi sehingga dapat diejek dengan percaya diri (ini jarang terjadi di dunia nyata) - unit test tidak cukup.

Semakin tinggi level tes Anda, semakin mereka akan melindungi Anda dari hal-hal yang tidak terduga. Itu membutuhkan biaya (kenyamanan, kecepatan, kerapuhan ...), jadi unit test harus tetap menjadi dasar pengujian Anda, tetapi Anda membutuhkan lapisan lain, termasuk - akhirnya - sedikit pengujian manusia yang sangat membantu dalam menangkap hal-hal bodoh yang tak seorang pun pikirkan.

ptyx
sumber
2

Cara terbaik adalah membuat prototipe minimal, dan memahami bagaimana perpustakaan bekerja dengan tepat. Dengan melakukan itu, Anda akan memperoleh pengetahuan tentang perpustakaan dengan dokumentasi yang buruk. Prototipe dapat berupa program minimalis yang menggunakan perpustakaan itu dan melakukan fungsinya.

Kalau tidak, tidak masuk akal untuk menulis unit test, dengan persyaratan setengah didefinisikan dan pemahaman sistem yang lemah.

Adapun masalah spesifik Anda - tentang menggunakan metrik yang salah: Saya akan memperlakukannya sebagai perubahan persyaratan. Setelah Anda mengenali masalah, ubah unit test dan kodenya.

BЈовић
sumber
1

Jika Anda menggunakan perpustakaan yang populer dan stabil, maka Anda mungkin bisa berasumsi bahwa itu tidak akan mempermainkan Anda. Tetapi jika hal-hal seperti apa yang Anda gambarkan terjadi dengan perpustakaan ini, maka jelas, ini bukan satu. Setelah pengalaman buruk ini, setiap kali ada yang salah dalam interaksi Anda dengan perpustakaan ini, Anda perlu memeriksa tidak hanya kemungkinan bahwa Anda telah membuat kesalahan, tetapi juga, kemungkinan bahwa perpustakaan mungkin telah membuat kesalahan. Jadi, katakanlah ini adalah perpustakaan yang Anda "tidak yakin" tentangnya.

Salah satu teknik yang digunakan dengan perpustakaan yang kami "tidak yakin" adalah dengan membangun lapisan menengah antara sistem kami dan perpustakaan tersebut, yang abstrak fungsi yang ditawarkan oleh perpustakaan, menegaskan bahwa harapan kami terhadap perpustakaan itu benar, dan juga sangat menyederhanakan hidup kita di masa depan, haruskah kita memutuskan untuk memberikan perpustakaan itu boot dan menggantinya dengan perpustakaan lain yang berperilaku lebih baik.

Mike Nakis
sumber
Ini tidak benar-benar menjawab pertanyaan. Saya sudah memiliki lapisan yang memisahkan perpustakaan dari sistem saya, tetapi masalahnya adalah lapisan abstraksi saya dapat memiliki "bug" ketika perpustakaan berubah pada saya tanpa peringatan.
durron597
1
@ durron597 Maka mungkin lapisan tidak cukup mengisolasi perpustakaan dari sisa aplikasi Anda. Jika Anda merasa kesulitan menguji lapisan itu, mungkin Anda perlu menyederhanakan perilaku dan lebih kuat mengisolasi data yang mendasarinya.
cbojar
Apa yang dikatakan @cbojar. Juga, izinkan saya mengulangi sesuatu yang mungkin tidak diperhatikan dalam teks di atas: Kata assertkunci (atau fungsi, atau fasilitas, tergantung pada bahasa apa yang Anda gunakan,) adalah teman Anda. Saya tidak berbicara tentang pernyataan dalam tes unit / integrasi, saya mengatakan bahwa lapisan isolasi harus sangat berat dengan pernyataan, menyatakan segala sesuatu yang tegas tentang perilaku perpustakaan.
Mike Nakis
Pernyataan ini tidak harus dijalankan pada saat produksi berjalan, tetapi mereka mengeksekusi saat pengujian, memiliki tampilan kotak putih dari lapisan isolasi Anda dan karena itu dapat memastikan (sebanyak mungkin) bahwa informasi yang diterima lapisan Anda dari perpustakaan adalah suara.
Mike Nakis