Apakah angka ajaib dapat diterima dalam tes unit jika angka tidak berarti apa-apa?

58

Dalam pengujian unit saya, saya sering melemparkan nilai arbitrer ke kode saya untuk melihat apa fungsinya. Misalnya, jika saya tahu foo(1, 2, 3)seharusnya mengembalikan 17, saya dapat menulis ini:

assertEqual(foo(1, 2, 3), 17)

Angka-angka ini murni arbitrer dan tidak memiliki makna yang lebih luas (mereka tidak, misalnya, syarat batas, meskipun saya juga mengujinya). Saya akan berjuang untuk menghasilkan nama-nama bagus untuk angka-angka ini, dan menulis sesuatu seperti const int TWO = 2;ini jelas tidak membantu. Apakah boleh menulis tes seperti ini, atau haruskah saya memasukkan angka ke dalam konstanta?

Dalam Apakah semua angka ajaib dibuat sama? , kami belajar bahwa angka ajaib baik-baik saja jika artinya jelas dari konteks, tetapi dalam kasus ini angka-angka itu sebenarnya tidak memiliki arti sama sekali.

Kevin
sumber
9
Jika Anda memasukkan nilai dan berharap dapat membaca nilai-nilai yang sama kembali, saya akan mengatakan angka ajaib baik-baik saja. Jadi jika, katakanlah, 1, 2, 3adalah indeks array 3D di mana Anda sebelumnya menyimpan nilai 17, maka saya pikir tes ini akan bagus (selama Anda memiliki beberapa tes negatif juga). Tetapi jika itu adalah hasil perhitungan, Anda harus memastikan bahwa siapa pun yang membaca tes ini akan mengerti mengapa foo(1, 2, 3)harus demikian 17, dan angka ajaib mungkin tidak akan mencapai tujuan itu.
Joe White
24
const int TWO = 2;bahkan lebih buruk daripada hanya menggunakan 2. Itu sesuai dengan kata-kata aturan dengan niat untuk melanggar semangatnya.
Agent_L
4
Apa nomor yang "tidak berarti apa-apa"? Mengapa itu ada dalam kode Anda jika itu tidak ada artinya?
Tim Grant
6
Tentu. Tinggalkan komentar sebelum serangkaian tes seperti itu, misalnya, "sejumlah kecil contoh yang ditentukan secara manual". Ini, terkait dengan pengujian Anda lainnya yang jelas menguji batas dan pengecualian, akan menjadi jelas.
davidbak
5
Contoh Anda menyesatkan - ketika nama fungsi Anda benar-benar foo, itu tidak akan berarti apa-apa, dan begitu pula parameternya. Namun dalam kenyataannya, saya cukup yakin fungsi tidak memiliki nama itu, dan parameter tidak memiliki nama bar1, bar2dan bar3. Buat contoh yang lebih realistis di mana nama memiliki makna, maka jauh lebih masuk akal untuk membahas jika nilai data uji juga membutuhkan nama.
Doc Brown

Jawaban:

80

Kapan Anda benar-benar memiliki angka yang tidak memiliki makna sama sekali?

Biasanya, ketika angka-angka memiliki makna, Anda harus menugaskan mereka ke variabel lokal dari metode pengujian untuk membuat kode lebih mudah dibaca dan menjelaskan diri sendiri. Nama-nama variabel setidaknya harus mencerminkan apa arti variabel, tidak harus nilainya.

Contoh:

const int startBalance = 10000;
const float interestRate = 0.05f;
const int years = 5;

const int expectedEndBalance = 12840;

assertEqual(calculateCompoundInterest(startBalance, interestRate, years),
            expectedEndBalance);

Perhatikan bahwa variabel pertama tidak bernama HUNDRED_DOLLARS_ZERO_CENT, tetapi startBalanceuntuk menunjukkan apa arti dari variabel tetapi tidak bahwa nilainya dalam hal apa pun istimewa.

Philipp
sumber
3
@Kevin - bahasa apa yang Anda uji? Beberapa kerangka kerja pengujian memungkinkan Anda mengatur penyedia data yang mengembalikan array array nilai untuk pengujian
HorusKol
10
Meskipun saya setuju dengan ide tersebut, berhati-hatilah karena praktik ini juga dapat menyebabkan kesalahan baru, seperti jika Anda secara tidak sengaja mengekstraksi nilai seperti 0.05fke int. :)
Jeff Bowman
5
+1 - hal-hal hebat. Hanya karena Anda tidak peduli berapa nilai tertentu itu, itu tidak berarti itu masih bukan angka ajaib ...
Robbie Dee
2
@PieterB: AFAIK itu kesalahan C dan C ++, yang memformalkan gagasan constvariabel.
Steve Jessop
2
Sudahkah Anda menamai variabel Anda sama dengan parameter bernama calculateCompoundInterest? Jika demikian, maka pengetikan ekstra adalah bukti kerja bahwa Anda telah membaca dokumentasi untuk fungsi yang Anda uji, atau setidaknya menyalin nama yang diberikan kepada Anda oleh IDE Anda. Saya tidak yakin berapa banyak ini memberi tahu pembaca tentang maksud kode, tetapi jika Anda melewati parameter dalam urutan yang salah setidaknya mereka bisa tahu apa yang dimaksudkan.
Steve Jessop
20

Jika Anda menggunakan angka arbitrer hanya untuk melihat apa yang mereka lakukan, maka apa yang sebenarnya Anda cari mungkin adalah data uji yang dibuat secara acak, atau pengujian berbasis properti.

Misalnya, Hipotesis adalah pustaka Python keren untuk pengujian semacam ini, dan didasarkan pada QuickCheck .

Pikirkan tes unit normal sebagai sesuatu seperti berikut ini:

  1. Siapkan beberapa data.
  2. Lakukan beberapa operasi pada data.
  3. Tunjukkan sesuatu tentang hasilnya.

Hipotesis memungkinkan Anda menulis tes yang malah terlihat seperti ini:

  1. Untuk semua data yang cocok dengan spesifikasi tertentu.
  2. Lakukan beberapa operasi pada data.
  3. Tunjukkan sesuatu tentang hasilnya.

Idenya adalah untuk tidak membatasi diri Anda dengan nilai-nilai Anda sendiri, tetapi pilih yang acak yang dapat digunakan untuk memeriksa apakah fungsi Anda cocok dengan spesifikasinya. Sebagai catatan penting, sistem ini umumnya akan mengingat setiap input yang gagal, dan kemudian memastikan bahwa input tersebut selalu diuji di masa depan.

Poin 3 dapat membingungkan bagi beberapa orang jadi mari kita perjelas. Itu tidak berarti bahwa Anda memberikan jawaban yang tepat - ini jelas tidak mungkin dilakukan untuk input sewenang-wenang. Alih-alih, Anda menegaskan sesuatu tentang properti hasil. Sebagai contoh, Anda mungkin menegaskan bahwa setelah menambahkan sesuatu ke daftar itu menjadi tidak kosong, atau bahwa pohon pencarian biner menyeimbangkan sebenarnya seimbang (menggunakan kriteria apa pun yang dimiliki struktur data tertentu).

Secara keseluruhan, memilih sendiri angka arbitrer mungkin sangat buruk - itu tidak benar-benar menambah sejumlah besar nilai, dan membingungkan bagi siapa pun yang membacanya. Secara otomatis menghasilkan banyak data uji acak dan menggunakannya secara efektif adalah baik. Menemukan Hipotesis atau pustaka seperti QuickCheck untuk bahasa pilihan Anda mungkin merupakan cara yang lebih baik untuk mencapai tujuan Anda sambil tetap dimengerti oleh orang lain.

Dannnno
sumber
11
Pengujian acak mungkin menemukan bug yang sulit direproduksi tetapi pengujian secara acak jarang menemukan bug yang dapat direproduksi. Pastikan untuk menangkap setiap kegagalan pengujian dengan case uji khusus yang dapat direproduksi.
JBRWilkinson
5
Dan bagaimana Anda tahu bahwa unit test Anda tidak disadap ketika Anda "menegaskan sesuatu tentang hasilnya" (dalam hal ini, hitung ulang apa fooitu komputasi) ...? Jika Anda 100% yakin kode Anda memberikan jawaban yang benar, maka Anda hanya akan memasukkan kode itu dalam program dan tidak mengujinya. Jika tidak, maka Anda perlu menguji, dan saya pikir semua orang melihat ke mana arahnya.
2
Ya, jika Anda memasukkan input acak ke dalam suatu fungsi, Anda harus tahu apa outputnya untuk dapat menyatakan bahwa itu berfungsi dengan benar. Dengan nilai tes yang tetap / dipilih, tentu saja Anda dapat mengerjakannya dengan tangan, dll. Tetapi tentu saja metode penentuan otomatis apa pun jika hasilnya benar akan mengalami masalah yang sama persis dengan fungsi yang Anda uji. Anda baik menggunakan implementasi yang Anda miliki (yang tidak dapat Anda lakukan karena Anda menguji apakah itu berfungsi) atau Anda menulis implementasi baru yang cenderung buggy (atau lebih banyak lagi Anda akan menggunakan yang lebih mungkin menjadi yang benar ).
Chris
7
@NajibIdrissi - belum tentu. Anda bisa, misalnya, menguji bahwa menerapkan kebalikan dari operasi yang Anda uji pada hasilnya memberikan kembali nilai awal yang Anda mulai. Atau Anda dapat menguji invarian yang diharapkan (mis. Untuk semua perhitungan bunga pada dhari, perhitungan pada dhari + 1 bulan harus diketahui tingkat persentase bulanannya lebih tinggi), dll.
Jules
12
@ Chris - Dalam banyak kasus, memeriksa hasil sudah benar lebih mudah daripada menghasilkan hasilnya. Meskipun ini tidak benar dalam semua keadaan, ada banyak di mana itu. Contoh: menambahkan entri ke pohon biner seimbang harus menghasilkan pohon baru yang juga seimbang ... mudah diuji, cukup rumit untuk diterapkan dalam praktik.
Jules
11

Nama tes unit Anda harus memberikan sebagian besar konteksnya. Bukan dari nilai konstanta. Nama / dokumentasi untuk suatu tes harus memberikan konteks yang sesuai dan penjelasan tentang angka ajaib apa pun yang ada dalam tes.

Jika itu tidak cukup, sedikit dokumentasi harus dapat menyediakannya (baik melalui nama variabel atau dokumen). Ingatlah bahwa fungsi itu sendiri memiliki parameter yang diharapkan memiliki nama yang bermakna. Menyalin mereka ke dalam pengujian Anda untuk menyebutkan argumen agak tidak ada gunanya.

Dan terakhir, jika unittests Anda cukup rumit sehingga ini sulit / tidak praktis Anda mungkin memiliki fungsi yang terlalu rumit dan mungkin mempertimbangkan mengapa hal ini terjadi.

Semakin ceroboh Anda menulis tes, semakin buruk kode aktual Anda. Jika Anda merasa perlu memberi nama nilai tes Anda untuk memperjelas tes, itu sangat menyarankan metode Anda yang sebenarnya membutuhkan penamaan dan / atau dokumentasi yang lebih baik. Jika Anda menemukan kebutuhan untuk menyebutkan konstanta dalam tes saya akan melihat mengapa Anda membutuhkan ini - kemungkinan masalahnya bukan tes itu sendiri tetapi implementasi

enderland
sumber
Jawaban ini tampaknya tentang kesulitan menyimpulkan tujuan suatu tes sedangkan pertanyaan yang sebenarnya adalah tentang angka ajaib dalam parameter metode ...
Robbie Dee
@RobbieDee nama / dokumentasi untuk ujian harus memberikan konteks dan penjelasan yang sesuai dari angka ajaib apa pun yang ada dalam ujian. Jika tidak, tambahkan dokumentasi atau ubah nama tes menjadi lebih jelas.
enderland
Masih akan lebih baik untuk memberikan nama angka ajaib. Jika jumlah parameter berubah, dokumentasi berisiko menjadi ketinggalan zaman.
Robbie Dee
1
@RobbieDee perlu diingat bahwa fungsi itu sendiri memiliki parameter yang mudah-mudahan memiliki nama yang bermakna. Menyalin mereka ke dalam pengujian Anda untuk menyebutkan argumen agak tidak ada gunanya.
enderland
"Mudah-mudahan" ya? Mengapa tidak hanya kode hal itu dengan benar dan menghilangkan apa yang tampaknya nomor ajaib seperti yang sudah dijabarkan Philipp ...
Robbie Dee
9

Ini sangat tergantung pada fungsi yang Anda uji. Saya tahu banyak kasus di mana angka-angka individual tidak memiliki arti khusus pada mereka sendiri, tetapi kasus uji secara keseluruhan dibangun dengan penuh pertimbangan dan karenanya memiliki makna tertentu. Itulah yang harus didokumentasikan seseorang dalam beberapa cara. Misalnya, jika foobenar-benar merupakan metode testForTriangleyang memutuskan apakah ketiga angka tersebut mungkin panjang tepian segitiga yang valid, pengujian Anda mungkin terlihat seperti ini:

// standard triangle with area >0
assertEqual(testForTriangle(2, 3, 4), true);

// degenerated triangle, length of two edges match the length of the third
assertEqual(testForTriangle(1, 2, 3), true);  

// no triangle
assertEqual(testForTriangle(1, 2, 4), false); 

// two sides equal
assertEqual(testForTriangle(2, 2, 3), true);

// all three sides equal
assertEqual(testForTriangle(4, 4, 4), true);

// degenerated triangle / point
assertEqual(testForTriangle(0, 0, 0), true);  

dan seterusnya. Anda dapat meningkatkan ini dan mengubah komentar menjadi parameter pesan assertEqualyang akan ditampilkan ketika tes gagal. Anda kemudian dapat meningkatkan ini lebih lanjut dan mengubahnya menjadi tes yang didorong data (jika kerangka pengujian Anda mendukung ini). Namun demikian, Anda dapat membantu diri sendiri jika Anda memasukkan catatan ke dalam kode mengapa Anda memilih angka ini dan dari berbagai perilaku yang Anda uji dengan masing-masing kasus.

Tentu saja, untuk fungsi lain, nilai individual untuk parameter mungkin lebih penting, jadi menggunakan nama fungsi yang tidak berarti seperti fooketika menanyakan cara menangani arti parameter mungkin bukan ide terbaik.

Doc Brown
sumber
Solusi yang masuk akal.
user1725145
6

Mengapa kita ingin menggunakan Konstanta yang dinamai bukan angka?

  1. KERING - Jika saya membutuhkan nilai di 3 tempat, saya hanya ingin mendefinisikannya sekali, jadi saya bisa mengubahnya di satu tempat, jika itu berubah.
  2. Memberi makna pada angka.

Jika Anda menulis beberapa tes unit, masing-masing dengan bermacam-macam 3 Angka (startBalance, bunga, tahun) - Saya hanya akan mengemas nilai-nilai ke dalam unit-test sebagai variabel lokal. Lingkup terkecil tempat mereka berada.

testBigInterest()
  var startBalance = 10;
  var interestInPercent = 100
  var years = 2
  assert( calcCreditSum( startBalance, interestInPercent, years ) == 40 )

testSmallInterest()
  var startBalance = 50;
  var interestInPercent = .5
  var years = 1
  assert( calcCreditSum( startBalance, interestInPercent, years ) == 50.25 )

Jika Anda menggunakan bahasa yang memungkinkan parameter bernama, ini tentu saja superflous. Di sana saya hanya akan mengemas nilai mentah dalam pemanggilan metode. Saya tidak bisa membayangkan refactoring membuat pernyataan ini lebih ringkas:

testBigInterest()
  assert( calcCreditSum( startBalance:       10
                        ,interestInPercent: 100
                        ,years:               2 ) = 40 )

Atau gunakan Kerangka Pengujian, yang akan memungkinkan Anda untuk menentukan kasus pengujian dalam beberapa array atau format Peta:

testcases = { {
                Name: "BigInterest"
               ,StartBalance:       10
               ,InterestInPercent: 100
               ,Years:               2
              }
             ,{ 
                Name: "SmallInterest"
               ,StartBalance:       50
               ,InterestInPercent:  .5
               ,Years:               1
              }
            }
Falco
sumber
3

... tetapi dalam hal ini angka sebenarnya tidak memiliki arti sama sekali

Angka-angka yang digunakan untuk memanggil metode jadi pasti premis di atas tidak benar. Anda mungkin tidak peduli berapa jumlahnya tetapi itu tidak penting. Ya, Anda dapat menyimpulkan angka-angka yang digunakan oleh beberapa sihir IDE tetapi akan jauh lebih baik jika Anda hanya memberikan nama nilai - bahkan jika mereka hanya cocok dengan parameternya.

Robbie Dee
sumber
1
Ini tidak selalu benar, meskipun - seperti dalam contoh unit test terbaru yang saya tulis ( assertEqual "Returned value" (makeKindInt 42) (runTest "lvalue_operators")). Dalam contoh ini, 42hanya nilai placeholder yang dihasilkan oleh kode dalam skrip uji yang dinamai lvalue_operatorsdan kemudian diperiksa saat dikembalikan oleh skrip. Tidak ada artinya sama sekali, selain itu nilai yang sama terjadi di dua tempat yang berbeda. Apa yang akan menjadi nama yang tepat di sini yang sebenarnya memberikan makna yang bermanfaat?
Jules
3

Jika Anda ingin menguji fungsi murni pada satu set input yang bukan kondisi batas, maka Anda hampir pasti ingin mengujinya pada sejumlah set input yang tidak (dan sedang) kondisi batas. Dan bagi saya itu berarti harus ada tabel nilai untuk memanggil fungsi, dan sebuah loop:

struct test_foo_values {
    int bar;
    int baz;
    int blurf;
    int expected;
};
const struct test_foo_values test_foo_with[] = {
   { 1, 2, 3, 17 },
   { 2, 4, 9, 34 },
   // ... many more here ...
};

for (size_t i = 0; i < ARRAY_SIZE(test_foo_with); i++) {
    const struct test_foo_values *c = test_foo_with[i];
    assertEqual(foo(c->bar, c->baz, c->blurf), c->expected);
}

Alat-alat seperti yang disarankan dalam jawaban Dannnno dapat membantu Anda menyusun tabel nilai untuk diuji. bar,, bazdan blurfharus diganti dengan nama-nama yang bermakna seperti dibahas dalam jawaban Philipp .

(Prinsip umum yang bisa diperdebatkan di sini: Angka tidak selalu "angka ajaib" yang perlu nama; sebaliknya, angka mungkin data . Jika masuk akal untuk memasukkan angka Anda ke dalam array, mungkin array catatan, maka mereka mungkin data Sebaliknya, jika Anda menduga Anda mungkin memiliki data di tangan Anda, pertimbangkan untuk memasukkannya ke dalam array dan memperoleh lebih banyak dari itu.)

zwol
sumber
1

Tes berbeda dari kode produksi dan, setidaknya dalam tes unit ditulis dalam Spock, yang singkat dan to the point, saya tidak punya masalah menggunakan konstanta sihir.

Jika suatu tes panjangnya 5 baris, dan mengikuti skema dasar diberikan / kapan / kemudian, mengekstraksi nilai-nilai tersebut ke dalam konstanta hanya akan membuat kode lebih panjang dan lebih sulit untuk dibaca. Jika logikanya adalah "Ketika saya menambahkan pengguna bernama Smith, saya melihat pengguna Smith kembali dalam daftar pengguna", tidak ada gunanya mengekstraksi "Smith" ke sebuah konstanta.

Ini tentu saja berlaku jika Anda dapat dengan mudah mencocokkan nilai yang digunakan di blok "diberikan" (setup) dengan yang ditemukan di blok "kapan" dan "kemudian". Jika pengaturan pengujian Anda dipisahkan (dalam kode) dari tempat data digunakan, mungkin lebih baik menggunakan konstanta. Tetapi karena tes terbaik dilakukan sendiri, pengaturan biasanya dekat dengan tempat penggunaan dan kasus pertama berlaku, yang berarti konstanta sihir cukup dapat diterima dalam kasus ini.

Michał Kosmulski
sumber
1

Pertama mari kita sepakat bahwa "unit test" sering digunakan untuk mencakup semua tes otomatis yang ditulis oleh seorang programmer, dan tidak ada gunanya memperdebatkan apa yang harus disebut setiap tes ....

Saya telah bekerja pada sistem di mana perangkat lunak mengambil banyak input dan bekerja di luar "solusi" yang harus memenuhi beberapa kendala, sementara mengoptimalkan angka lainnya. Tidak ada jawaban yang benar, sehingga perangkat lunak hanya harus memberikan jawaban yang masuk akal.

Itu melakukan ini dengan menggunakan banyak angka acak untuk mendapatkan titik awal, kemudian menggunakan "pendaki bukit" untuk meningkatkan hasilnya. Ini dijalankan berkali-kali, memilih hasil terbaik. Generator bilangan acak dapat diunggulkan, sehingga selalu memberikan angka yang sama dalam urutan yang sama, maka jika tes menetapkan seed, kita tahu bahwa hasilnya akan sama pada setiap run.

Kami memiliki banyak tes yang melakukan hal di atas, dan memeriksa bahwa hasilnya sama, ini memberi tahu kami bahwa kami tidak mengubah apa yang dilakukan bagian sistem secara tidak sengaja saat refactoring dll. Tidak memberi tahu kami apa pun tentang kebenaran dari apa yang bagian dari sistem lakukan.

Tes-tes ini mahal untuk dipertahankan, karena setiap perubahan pada kode pengoptimalan akan mematahkan tes, tetapi mereka juga menemukan beberapa bug dalam kode yang jauh lebih besar yang melakukan pra-pemrosesan data, dan pasca-memproses hasilnya.

Saat kami "mengejek" database, Anda bisa menyebut tes ini "tes unit", tetapi "unit" itu agak besar.

Seringkali ketika Anda bekerja pada sistem tanpa tes, Anda melakukan sesuatu seperti di atas, sehingga Anda dapat mengonfirmasi refactoring Anda tidak mengubah output; semoga tes yang lebih baik ditulis untuk kode baru!

Ian
sumber
1

Saya pikir dalam kasus ini angka-angka harus disebut Angka Sewenang-wenang, bukan Angka Ajaib, dan hanya berkomentar garis sebagai "kasus uji sewenang-wenang".

Tentu saja, beberapa Angka Ajaib juga dapat berubah-ubah, seperti untuk nilai "pegangan" yang unik (yang harus diganti dengan konstanta bernama, tentu saja), tetapi juga dapat dihitung sebagai konstanta seperti "kecepatan udara burung pipit Eropa yang tidak dikenali dalam furlongs per dua minggu", di mana nilai numerik dicolokkan tanpa komentar atau konteks bermanfaat.

RufusVS
sumber
0

Saya tidak akan berani mengatakan ya / tidak, tapi di sini ada beberapa pertanyaan yang harus Anda tanyakan pada diri sendiri ketika memutuskan apakah itu OK atau tidak.

  1. Jika angkanya tidak berarti apa-apa, mengapa mereka ada di tempat pertama? Bisakah mereka diganti dengan yang lain? Bisakah Anda melakukan verifikasi berdasarkan pada pemanggilan metode dan aliran alih-alih pernyataan nilai? Pertimbangkan sesuatu seperti verify()metode Mockito yang memeriksa apakah pemanggilan metode tertentu dilakukan untuk mengolok-olok objek dan bukannya benar-benar menegaskan nilai.

  2. Jika angka-angka memang berarti sesuatu, maka mereka harus ditugaskan ke variabel yang diberi nama dengan tepat.

  3. Menulis nomor 2sebagai TWOmungkin bermanfaat dalam konteks tertentu, dan tidak begitu banyak dalam konteks lain.

    • Misalnya: assertEquals(TWO, half_of(FOUR))masuk akal bagi seseorang yang membaca kode. Segera jelas apa yang Anda uji.
    • Namun jika tes Anda assertEquals(numCustomersInBank(BANK_1), TWO), maka ini tidak membuat yang banyak akal. Mengapa tidak BANK_1mengandung dua pelanggan? Untuk apa kita menguji?
Arnab Datta
sumber