Apa tes unit yang baik untuk menutupi kasus penggunaan menggulung mati?

18

Saya mencoba memahami unit testing.

Katakanlah kita memiliki dadu yang dapat memiliki jumlah sisi standar sama dengan 6 (tetapi dapat berupa 4, 5 sisi, dll.):

import random
class Die():
    def __init__(self, sides=6):
        self._sides = sides

    def roll(self):
        return random.randint(1, self._sides)

Apakah yang berikut ini akan menjadi tes unit yang valid / berguna?

  • uji gulungan dalam kisaran 1-6 untuk cetakan 6 sisi
  • uji gulungan 0 untuk dadu 6 sisi
  • uji gulungan 7 untuk dadu 6 sisi
  • uji gulungan dalam kisaran 1-3 untuk dadu 3 sisi
  • uji gulungan 0 untuk dadu 3 sisi
  • uji gulungan 4 untuk dadu 3 sisi

Saya hanya berpikir bahwa ini adalah buang-buang waktu karena modul acak telah ada cukup lama tetapi kemudian saya pikir jika modul acak diperbarui (katakanlah saya memperbarui versi Python saya) maka setidaknya saya dibahas.

Juga, apakah saya bahkan perlu menguji variasi lain dari gulungan mati misalnya 3 dalam hal ini, atau apakah baik untuk mencakup keadaan cetakan lain yang sudah diinisialisasi?

Cybran
sumber
1
Bagaimana dengan die-sisi minus 5, atau die-sisi null?
JensG

Jawaban:

22

Anda benar, tes Anda seharusnya tidak memverifikasi bahwa randommodul melakukan tugasnya; unittest seharusnya hanya menguji kelas itu sendiri, bukan bagaimana ia berinteraksi dengan kode lain (yang harus diuji secara terpisah).

Tentu saja sangat mungkin kode Anda random.randint()salah; atau Anda random.randrange(1, self._sides)malah menelepon dan mati Anda tidak pernah melempar nilai tertinggi, tetapi itu akan menjadi jenis bug yang berbeda, bukan bug yang bisa Anda tangkap dengan yang paling sedikit. Jika demikian, die unit Anda berfungsi seperti yang dirancang, tetapi desainnya sendiri cacat.

Dalam hal ini, saya akan menggunakan mengejek untuk menggantikan yang randint()fungsi, dan hanya memverifikasi bahwa telah disebut dengan benar. Python 3.3 dan yang lebih baru hadir dengan unittest.mockmodul untuk menangani jenis pengujian ini, tetapi Anda dapat menginstal mockpaket eksternal pada versi yang lebih lama untuk mendapatkan fungsionalitas yang sama persis

import unittest
try:
    from unittest.mock import patch
except ImportError:
    # < python 3.3
    from mock import patch


@patch('random.randint', return_value=3)
class TestDice(unittest.TestCase):
    def _make_one(self, *args, **kw):
        from die import Die
        return Die(*args, **kw)

    def test_standard_size(self, mocked_randint):
        die = self._make_one()
        result = die.roll()

        mocked_randint.assert_called_with(1, 6)
        self.assertEqual(result, 3)

    def test_custom_size(self, mocked_randint):
        die = self._make_one(sides=42)
        result = die.roll()

        mocked_randint.assert_called_with(1, 42)
        self.assertEqual(result, 3)


if __name__ == '__main__':
    unittest.main()

Dengan mengejek, tes Anda sekarang sangat sederhana; hanya ada 2 kasus, sungguh. Kasing default untuk dadu 6 sisi, dan kasing sisi khusus.

Ada cara lain untuk mengganti sementara randint()fungsi dalam namespace global Die, tetapi mockmodul membuatnya lebih mudah. The @mock.patchdekorator di sini berlaku untuk semua metode pengujian dalam kasus uji; setiap metode pengujian dilewatkan argumen tambahan, random.randint()fungsi yang diolok-olok , sehingga kita dapat menguji terhadap tiruan untuk melihat apakah itu memang telah dipanggil dengan benar. The return_valueArgumen menspesifikasikan apa yang kembali dari mock ketika itu disebut, sehingga kami dapat memverifikasi bahwa die.roll()metode memang mengembalikan 'acak' hasil kepada kami.

Saya telah menggunakan praktik terbaik unittesting Python lain di sini: impor kelas yang sedang diuji sebagai bagian dari tes. The _make_oneMetode melakukan impor dan Instansiasi bekerja dalam tes , sehingga tes modul masih akan memuat bahkan jika Anda membuat kesalahan sintaks atau kesalahan lain yang akan mencegah modul asli untuk impor.

Dengan cara ini, jika Anda membuat kesalahan dalam kode modul itu sendiri, tes akan tetap dijalankan; mereka hanya akan gagal, memberi tahu Anda tentang kesalahan dalam kode Anda.

Agar jelas, tes di atas adalah sederhana di ekstrem. Tujuannya di sini bukan untuk menguji yang random.randint()telah dipanggil dengan argumen yang tepat, misalnya. Sebaliknya, tujuannya adalah untuk menguji bahwa unit menghasilkan hasil yang tepat diberikan input tertentu, di mana input tersebut mencakup hasil unit lain yang tidak diuji. Dengan mengejek random.randint()metode ini, Anda bisa mengendalikan hanya input lain ke kode Anda.

Dalam tes dunia nyata , kode aktual dalam unit-under-test Anda akan menjadi lebih kompleks; hubungan dengan input yang diteruskan ke API dan bagaimana unit lain dipanggil dapat tetap menarik, dan mengejek akan memberi Anda akses ke hasil antara, serta memungkinkan Anda menetapkan nilai balik untuk panggilan tersebut.

Misalnya, dalam kode yang mengautentikasi pengguna terhadap layanan OAuth2 pihak ketiga (interaksi multi-tahap), Anda ingin menguji bahwa kode Anda meneruskan data yang benar ke layanan pihak ke-3 itu, dan memungkinkan Anda mengejek berbagai respons kesalahan yang berbeda yang Layanan pihak ketiga akan kembali, memungkinkan Anda mensimulasikan skenario yang berbeda tanpa harus membangun sendiri server OAuth2 lengkap. Di sini penting untuk menguji bahwa informasi dari respons pertama telah ditangani dengan benar dan telah diteruskan ke panggilan tahap kedua, jadi Anda ingin melihat bahwa layanan yang dipermainkan dipanggil dengan benar.

Martijn Pieters
sumber
1
Anda mendapatkan beberapa lebih dari 2 kasus pengujian ... hasil memeriksa nilai default: lebih rendah (1), atas (6), di bawah lebih rendah (0), di atas (7) dan hasil untuk angka yang ditentukan pengguna seperti max_int dll. input juga tidak divalidasi, yang mungkin perlu diuji di beberapa titik ...
James Snell
2
Tidak, itu tes untuk randint(), bukan kode masuk Die.roll().
Martijn Pieters
Sebenarnya ada cara untuk memastikan bahwa bukan hanya randint yang dipanggil dengan benar tetapi juga hasilnya digunakan dengan benar: mengejeknya untuk mengembalikan sentinel.diecontoh (objek sentinel juga dari unittest.mock) dan kemudian memverifikasi bahwa itu adalah apa yang dikembalikan dari metode roll Anda. Ini sebenarnya hanya memungkinkan satu cara menerapkan metode yang diuji.
aragaer
@aragaer: yakin, jika Anda ingin memverifikasi bahwa nilai yang dikembalikan tidak berubah, sentinel.dieakan menjadi cara yang bagus untuk memastikannya.
Martijn Pieters
Saya tidak mengerti mengapa Anda ingin memastikan bahwa mocked_randint dipanggil_dengan nilai-nilai tertentu. Saya mengerti ingin mengejek Randand untuk mengembalikan nilai-nilai yang dapat diprediksi, tetapi bukankah hanya kekhawatiran bahwa ia mengembalikan nilai-nilai yang dapat diprediksi dan bukan nilai-nilai apa yang disebutnya? Sepertinya saya bahwa memeriksa nilai-nilai yang dipanggil tidak perlu mengikat tes untuk detail implementasi yang baik. Juga mengapa kita peduli bahwa cetakan mengembalikan nilai tepat randint? Bukankah kita benar-benar peduli bahwa itu mengembalikan nilai> 1 dan kurang dari sama dengan maks?
bdrx
16

Jawaban Martijn adalah bagaimana Anda akan melakukannya jika Anda benar-benar ingin menjalankan tes yang menunjukkan bahwa Anda menelepon secara acak. Brandint. Namun, dengan risiko diberi tahu "itu tidak menjawab pertanyaan", saya merasa ini seharusnya tidak diuji unit sama sekali. Mengejek Randand tidak lagi pengujian kotak hitam - Anda secara khusus menunjukkan bahwa hal-hal tertentu sedang terjadi dalam implementasi . Pengujian black box bahkan bukan opsi - tidak ada tes yang dapat Anda lakukan yang akan membuktikan bahwa hasilnya tidak akan pernah kurang dari 1 atau lebih dari 6.

Bisakah kamu mengejek randint? Ya kamu bisa. Tapi apa yang kamu buktikan? Anda menyebutnya dengan argumen 1 dan sisi. Apa artinya itu ? Anda kembali ke titik awal - pada akhirnya Anda harus membuktikan - secara formal atau tidak formal - bahwa panggilan random.randint(1, sides)dengan benar akan menerapkan roll dadu.

Saya semua untuk pengujian unit. Mereka cek kewarasan fantastis dan mengekspos kehadiran bug. Namun, mereka tidak pernah dapat membuktikan ketidakhadiran mereka, dan ada hal-hal yang tidak dapat ditegaskan melalui pengujian sama sekali (misalnya bahwa fungsi tertentu tidak pernah melempar pengecualian atau selalu berakhir.) Dalam kasus khusus ini, saya merasa ada sangat sedikit Anda berdiri untuk menguji mendapatkan. Untuk perilaku yang deterministik, unit test masuk akal karena Anda benar-benar tahu apa jawaban yang Anda harapkan.

Doval
sumber
Tes unit bukan tes kotak hitam, sungguh. Itulah tujuan dari tes integrasi, untuk memastikan bahwa berbagai bagian berinteraksi seperti yang dirancang. Ini masalah pendapat, tentu saja (sebagian besar filsafat pengujian), lihat Apakah "Pengujian Unit" berada di bawah pengujian kotak putih atau kotak hitam? dan Black Box Unit Testing untuk beberapa perspektif (Stack Overflow).
Martijn Pieters
@ MartijnPieters Saya tidak setuju bahwa "itulah tujuan dari tes integrasi". Tes integrasi adalah untuk memeriksa apakah semua komponen sistem berinteraksi dengan benar. Mereka bukan tempat untuk menguji bahwa komponen yang diberikan memberikan output yang benar untuk input yang diberikan. Sedangkan untuk pengujian kotak hitam vs unit kotak putih, pengujian unit kotak putih pada akhirnya akan pecah dengan perubahan implementasi, dan setiap asumsi yang Anda buat dalam implementasi kemungkinan akan terbawa ke dalam pengujian. Memvalidasi yang random.randintdisebut dengan 1, sidestidak berguna jika itu hal yang salah untuk dilakukan.
Doval
Ya, itu adalah batasan dari tes unit kotak putih. Namun, tidak ada titik dalam pengujian yang random.randint()akan mengembalikan nilai dalam rentang [1, sisi] dengan benar (inklusif), terserah pengembang Python untuk memastikan bahwa randomunit bekerja dengan benar.
Martijn Pieters
Dan seperti yang Anda katakan sendiri, pengujian unit tidak dapat menjamin bahwa kode Anda bebas bug; jika kode Anda menggunakan unit lain secara salah (katakanlah, Anda diharapkan random.randint()berperilaku seperti random.randrange()dan dengan demikian menyebutnya random.randint(1, sides + 1), maka Anda tetap saja tenggelam
Martijn Pieters
2
@ MartijnPieters Saya setuju dengan Anda di sana, tapi bukan itu yang saya keberatan. Saya keberatan untuk menguji bahwa random.randint dipanggil dengan argumen (1, sisi) . Anda telah berasumsi dalam implementasi bahwa ini adalah hal yang benar untuk dilakukan, dan sekarang Anda mengulangi asumsi itu dalam ujian. Jika anggapan itu salah, tes akan berlalu tetapi implementasi Anda masih salah. Ini adalah bukti setengah-setengah yang merupakan rasa sakit penuh untuk ditulis dan dipelihara.
Doval
6

Memperbaiki benih acak. Untuk dadu bersisi 1, 2, 5, dan 12 sisi, pastikan bahwa beberapa ribu gulungan memberikan hasil termasuk 1 dan N, dan tidak termasuk 0 atau N + 1. Jika secara kebetulan Anda mendapatkan serangkaian hasil acak yang tidak tutup kisaran yang diharapkan, ganti dengan seed yang berbeda.

Alat mengejek itu keren, tetapi hanya karena memungkinkan Anda melakukan sesuatu, tidak berarti hal itu harus dilakukan. YAGNI berlaku untuk perlengkapan pengujian sebanyak fitur.

Jika Anda dapat dengan mudah menguji dengan dependensi yang tidak diolok-olok, Anda harus selalu melakukannya; dengan cara itu tes Anda akan difokuskan pada pengurangan jumlah cacat, tidak hanya meningkatkan jumlah tes. Kelebihan risiko mencibir menciptakan angka cakupan menyesatkan, yang pada gilirannya dapat menyebabkan menunda pengujian yang sebenarnya ke beberapa tahap selanjutnya Anda mungkin tidak pernah punya waktu untuk ...

soru
sumber
3

Apa itu Diejika Anda memikirkannya? - tidak lebih dari pembungkus random. Hal ini merangkum random.randintdan relabels itu dalam hal kosakata aplikasi Anda sendiri: Die.Roll.

Saya tidak merasa relevan untuk menyisipkan lapisan abstraksi lain di antara Diedan randomkarena Dieitu sendiri sudah merupakan lapisan tipuan antara aplikasi Anda dan platform.

Jika Anda ingin hasil dadu kalengan, hanya mengejek Die, jangan mengejekrandom .

Secara umum, saya tidak menguji unit objek pembungkus saya yang berkomunikasi dengan sistem eksternal, saya menulis tes integrasi untuk mereka. Anda dapat menulis beberapa untuk Dietetapi seperti yang Anda tunjukkan, karena sifat acak dari objek yang mendasarinya, mereka tidak akan bermakna. Selain itu, tidak ada konfigurasi atau komunikasi jaringan yang terlibat di sini sehingga tidak banyak untuk diuji kecuali panggilan platform.

=> Menimbang bahwa Diehanya beberapa baris kode yang sepele dan menambahkan sedikit atau tidak ada logika dibandingkan dengan randomdirinya sendiri, saya akan melewatkan pengujian dalam contoh khusus itu.

guillaume31
sumber
2

Menyemai generator angka acak dan memverifikasi hasil yang diharapkan BUKAN, sejauh yang saya bisa lihat, tes yang valid. Itu membuat asumsi BAGAIMANA dadu Anda bekerja secara internal, yang nakal-nakal. Pengembang python dapat mengubah generator angka acak, atau die (CATATAN: "dadu" adalah jamak, "die" adalah singular. Kecuali jika kelas Anda mengimplementasikan beberapa die rolls dalam satu panggilan, mungkin seharusnya disebut "die") bisa menggunakan generator nomor acak yang berbeda.

Demikian pula, mengejek fungsi acak mengasumsikan bahwa implementasi kelas bekerja persis seperti yang diharapkan. Mengapa ini tidak terjadi? Seseorang mungkin mengendalikan generator nomor acak python default, dan untuk menghindarinya, versi die Anda yang akan datang mungkin mengambil beberapa angka acak, atau angka acak yang lebih besar, untuk digabungkan dengan lebih banyak data acak. Skema yang sama digunakan oleh pembuat sistem operasi FreeBSD, ketika mereka menduga NSA merusak perangkat keras nomor acak generator yang dibangun ke dalam CPU.

Jika itu saya, saya akan menjalankan, katakanlah, 6000 gulungan, menghitungnya, dan memastikan bahwa setiap angka dari 1-6 digulung antara 500 dan 1500 kali. Saya juga akan memeriksa bahwa tidak ada angka di luar rentang yang dikembalikan. Saya mungkin juga memeriksa bahwa, untuk set kedua 6000 gulungan, ketika memesan [1..6] dalam urutan frekuensi, hasilnya berbeda (ini akan gagal sekali dari 720 run, jika jumlahnya acak!). Jika Anda ingin teliti, Anda mungkin menemukan frekuensi angka mengikuti 1, mengikuti 2, dll; tetapi pastikan ukuran sampel Anda cukup besar, dan Anda memiliki varians yang cukup. Manusia berharap angka acak memiliki pola lebih sedikit daripada yang sebenarnya.

Ulangi selama 12 sisi, dan 2 sisi mati (6 adalah yang paling sering digunakan, demikian juga yang paling diharapkan bagi siapa pun yang menulis kode ini).

Akhirnya, saya akan menguji untuk melihat apa yang terjadi dengan dadu 1 sisi, dadu 0 sisi, dadu sisi -1, dadu sisi 2,3, dadu sisi [1,2,3,4,5,6], dan mati "blah". Tentu saja, ini semua harus gagal; apakah mereka gagal dengan cara yang bermanfaat? Ini mungkin gagal pada saat penciptaan, bukan pada penguliran.

Atau, mungkin, Anda ingin menangani ini secara berbeda - mungkin membuat cetakan dengan [1,2,3,4,5,6] harus dapat diterima - dan mungkin "bla" juga; ini mungkin mati dengan 4 wajah, dan setiap wajah memiliki huruf di atasnya. Permainan "Boggle" muncul dalam pikiran, seperti halnya bola ajaib delapan.

Dan akhirnya, Anda mungkin ingin merenungkan ini: http://lh6.ggpht.com/-fAGXwbJbYRM/UJA_31ACOLI/AAAAAAAAAPg/2FxOWzo96KE/s1600-h/random%25255B3%25255D.jpg

AMADANON Inc.
sumber
2

Dengan risiko berenang melawan arus, saya memecahkan masalah yang tepat ini beberapa tahun yang lalu menggunakan metode yang sejauh ini tidak disebutkan.

Strategi saya hanyalah mengejek RNG dengan yang menghasilkan aliran nilai yang dapat diprediksi yang mencakup seluruh ruang. Jika (katakanlah) sisi = 6 dan RNG menghasilkan nilai dari 0 hingga 5 secara berurutan, saya dapat memprediksi bagaimana kelas saya harus berperilaku dan unit test sesuai.

Alasannya adalah bahwa ini menguji logika di kelas ini saja, dengan asumsi bahwa RNG pada akhirnya akan menghasilkan masing-masing nilai-nilai tersebut dan tanpa menguji RNG itu sendiri.

Sederhana, deterministik, dapat direproduksi, dan dapat menangkap bug. Saya akan menggunakan strategi yang sama lagi.


Pertanyaannya tidak menjabarkan apa tes yang seharusnya, hanya data apa yang mungkin digunakan untuk pengujian, mengingat adanya RNG. Saran saya hanyalah menguji secara menyeluruh dengan mengejek RNG. Pertanyaan tentang apa yang layak diuji tergantung pada informasi yang tidak disediakan dalam pertanyaan.

david.pfx
sumber
Katakanlah Anda mengejek RNG agar dapat diprediksi. Lalu apa yang akan Anda uji? Pertanyaannya bertanya "Apakah yang berikut ini akan menjadi tes unit yang valid / berguna?" Mengejeknya untuk mengembalikan 0-5 bukan tes melainkan pengaturan tes. Bagaimana Anda "menguji unit sesuai"? Saya gagal memahami bagaimana "menangkap bug". Saya mengalami kesulitan memahami apa yang saya butuhkan untuk tes 'unit'.
bdrx
@bdrx: Ini beberapa waktu yang lalu: Saya akan menjawabnya secara berbeda sekarang. Tapi lihat sunting.
david.pfx
1

Tes yang Anda sarankan dalam pertanyaan Anda tidak mendeteksi penghitung aritmatika modular sebagai implementasi. Dan mereka tidak mendeteksi kesalahan implementasi umum dalam kode terkait distribusi probabilitas seperti return 1 + (random.randint(1,maxint) % sides). Atau perubahan generator yang menghasilkan pola 2 dimensi.

Jika Anda benar-benar ingin memverifikasi bahwa Anda menghasilkan angka acak yang terdistribusi secara merata, Anda perlu memeriksa berbagai properti yang sangat luas. Untuk melakukan pekerjaan yang cukup baik pada saat itu Anda bisa menjalankan http://www.phy.duke.edu/~rgb/General/dieharder.php pada nomor yang Anda hasilkan. Atau tulislah unit-test suite yang serupa rumitnya.

Itu bukan kesalahan unit-testing atau TDD, keacakan kebetulan menjadi properti yang sangat sulit untuk diverifikasi. Dan topik populer sebagai contoh.

Patrick
sumber
-1

Tes termudah dari die-roll adalah hanya mengulanginya beberapa ratus ribu kali, dan memvalidasi bahwa setiap hasil yang mungkin dipukul kira-kira (1 / jumlah sisi) kali. Dalam kasus dadu 6 sisi, Anda harus melihat setiap nilai yang mungkin mengenai sekitar 16,6% dari waktu. Jika ada yang mati lebih dari satu persen, maka Anda memiliki masalah.

Melakukannya dengan cara ini menghindari memungkinkan Anda untuk memperbaiki mekanisme yang mendasari menghasilkan angka acak dengan mudah, dan yang paling penting, tanpa mengubah tes.

ChristopherBrown
sumber
1
tes ini akan lulus untuk implementasi yang benar-benar non-acak yang hanya loop melalui sisi satu per satu dalam urutan yang telah ditentukan
nyamuk
1
Jika seorang pembuat kode bermaksud mengimplementasikan sesuatu dengan itikad buruk (tidak menggunakan agen pengacakan pada cetakan), dan hanya mencoba menemukan sesuatu untuk 'membuat lampu merah berubah hijau' Anda memiliki lebih banyak masalah daripada yang dapat dipecahkan oleh unit test.
ChristopherBrown