Apakah secara implisit tergantung pada fungsi murni yang buruk (khususnya, untuk pengujian)?

8

Untuk memperluas sedikit pada judul, saya mencoba untuk mendapatkan beberapa kesimpulan tentang apakah perlu atau tidak untuk secara eksplisit mendeklarasikan (yaitu menyuntikkan) fungsi murni di mana beberapa fungsi atau kelas lain bergantung.

Apakah kode yang diberikan kurang dapat diuji atau lebih buruk dirancang jika menggunakan fungsi murni tanpa meminta mereka? Saya ingin sampai pada kesimpulan tentang masalah ini, untuk segala jenis fungsi murni dari fungsi sederhana, asli (misalnya max(), min()- terlepas dari bahasa) ke kebiasaan, yang lebih rumit yang pada gilirannya mungkin secara implisit tergantung pada fungsi murni lainnya.

Perhatian yang biasa adalah bahwa jika beberapa kode hanya menggunakan dependensi secara langsung, Anda tidak akan dapat mengujinya secara terpisah, yaitu pada saat yang sama Anda akan menguji semua hal yang Anda bawa secara diam-diam bersama Anda. Tapi ini menambahkan beberapa boilerplate jika Anda harus melakukannya untuk setiap fungsi kecil, jadi saya ingin tahu apakah ini masih berlaku untuk fungsi murni, dan mengapa atau mengapa tidak.

DanielM
sumber
2
Saya tidak mengerti pertanyaan ini. Apa bedanya jika fungsi murni untuk tujuan DI?
Telastyn
Jadi Anda mengusulkan bahwa fungsi murni, fungsi tidak murni dan kelas disuntikkan semua sama, tidak peduli ukurannya, dependensi transitif atau apa pun. Bisakah Anda menguraikan hal itu?
DanielM
Tidak, saya hanya tidak mengerti apa yang Anda minta. Fungsi jarang disuntikkan ke dalam apa pun, dan ketika itu, konsumen tidak dapat menjamin kemurnian mereka.
Telastyn
1
Anda dapat menyuntikkan setiap fungsionalitas, tetapi sering kali tidak ada nilai dalam mengejek fungsi-fungsi ini dalam sebuah tes yang berarti bahwa menyuntikkan mereka tidak memiliki nilai. Misalnya, karena operator pada dasarnya adalah fungsi statis, seseorang dapat memutuskan untuk menyuntikkan operator + dan mengejeknya dalam suatu pengujian. Kecuali itu memiliki nilai, Anda tidak akan melakukan itu.
user2180613
1
Operator juga terlintas di benak saya. Tentu saja akan merepotkan, tetapi poin yang saya coba temukan adalah bagaimana membuat keputusan tentang apa yang harus disuntikkan dan yang tidak. Dengan kata lain, bagaimana memutuskan kapan ada nilai dan kapan tidak. Terinspirasi pada jawaban @ Goyo, saya pikir sekarang bahwa kriteria yang baik adalah menyuntikkan segala sesuatu yang bukan bagian intrinsik dari sistem, untuk menjaga sistem dan dependensinya digabungkan secara longgar; dan sebaliknya, gunakan saja (dengan demikian, jangan menyuntikkan) hal-hal yang benar-benar merupakan bagian dari identitas sistem, yang dengannya sistem tersebut sangat kohesif.
DanielM

Jawaban:

10

Tidak, itu tidak buruk

Tes yang Anda tulis seharusnya tidak peduli bagaimana kelas atau fungsi tertentu diimplementasikan. Sebaliknya, harus memastikan bahwa mereka menghasilkan hasil yang Anda inginkan terlepas dari bagaimana tepatnya mereka diterapkan.

Sebagai contoh, pertimbangkan kelas berikut:

Coord2d{
    float x, y;

    ///Will round to the nearest whole number
    Coord2d Round();
}

Anda ingin menguji fungsi 'Putaran' untuk memastikan bahwa ia mengembalikan apa yang Anda harapkan, terlepas dari bagaimana sebenarnya fungsi tersebut diimplementasikan. Anda mungkin akan menulis tes yang mirip dengan yang berikut ini;

Coord2d testValue(0.6, 0.1)
testValueRounded = testValue.Round()
CHECK( testValueRounded.x == 1.0 )
CHECK( testValueRounded.y == 0.0 )

Dari sudut pandang pengujian, selama fungsi Coord2d :: Round () Anda mengembalikan apa yang Anda harapkan, Anda tidak peduli bagaimana penerapannya.

Dalam beberapa kasus, menyuntikkan fungsi murni bisa menjadi cara yang sangat brilian untuk membuat kelas lebih dapat diuji atau diperluas.

Namun, dalam kebanyakan kasus, seperti fungsi Coord2d :: Round () di atas, tidak ada kebutuhan nyata untuk menyuntikkan fungsi min / max / floor / ceil / trunc. Fungsi ini dirancang untuk membulatkan nilainya ke bilangan bulat terdekat. Tes yang Anda tulis harus memeriksa apakah fungsi melakukan ini.

Terakhir, jika Anda ingin menguji kode yang bergantung pada implementasi kelas / fungsi, Anda dapat melakukannya dengan hanya menulis tes untuk dependensi.

Misalnya, jika fungsi Coord2d :: Round () diimplementasikan seperti ...

Coord2d Round(){
    return Coord2d( floor(x + 0.5f),  floor(y + 0.5f))
}

Jika Anda ingin menguji fungsi 'lantai', Anda dapat melakukannya di unit test terpisah.

CHECK( floor (1.436543) == 1.0)
CHECK( floor (134.936) == 134.0)
Ryan
sumber
Semua yang Anda katakan mengacu pada fungsi murni, bukan? Jika demikian, apa bedanya dengan yang tidak murni atau kelas? Maksud saya, Anda bisa saja mengkodekan semuanya dan menerapkan logika yang sama ("semua yang saya andalkan sudah diuji"). Dan pertanyaan yang berbeda: dapatkah Anda memperluas kapan akan brilian untuk menyuntikkan fungsi murni dan mengapa? Terima kasih!
DanielM
3
@DanielM Tentu saja - unit test harus mencakup beberapa unit terisolasi dari perilaku yang bermakna - jika validitas perilaku "unit" sangat tergantung pada pengembalian maks dari sesuatu, maka mengabstraksi "max" tidak berguna dan benar-benar mengurangi kegunaannya dari tes juga. Di sisi lain, jika saya mengembalikan "maks" dari apa pun yang berasal dari beberapa penyedia, maka mematikan penyedia berguna, karena apa yang dilakukannya tidak secara langsung berkaitan dengan perilaku yang dimaksudkan dari unit ini dan kebenarannya dapat diverifikasi di tempat lain. Ingat - "unit"! = "Kelas."
Semut P
2
Namun, pada akhirnya, terlalu memikirkan hal semacam ini biasanya kontraproduktif. Di sinilah tes pertama cenderung menjadi miliknya sendiri. Tetapkan perilaku terisolasi yang ingin Anda uji, kemudian tulis tes dan keputusan tentang apa yang harus disuntikkan dan apa yang tidak disuntikkan dibuat untuk Anda.
Ant P
4

Secara implisit tergantung pada fungsi murni yang buruk

Dari sudut pandang pengujian - Tidak, tetapi hanya dalam kasus fungsi murni , ketika fungsi mengembalikan output yang sama untuk input yang sama.

Bagaimana Anda menguji unit yang menggunakan fungsi yang disuntikkan secara eksplisit?
Anda akan menyuntikkan fungsi mocked (palsu) dan memeriksa fungsi yang dipanggil dengan argumen yang benar.

Tetapi karena kita memiliki fungsi murni , yang selalu mengembalikan hasil yang sama untuk argumen yang sama - memeriksa argumen input sama dengan memeriksa hasil keluaran.

Dengan fungsi murni Anda tidak perlu mengonfigurasi dependensi / status tambahan.

Sebagai manfaat tambahan Anda akan mendapatkan enkapsulasi yang lebih baik, Anda akan menguji perilaku unit yang sebenarnya.
Dan sangat penting dari sudut pandang pengujian - Anda akan bebas untuk merefactor kode internal unit tanpa mengubah tes - misalnya Anda memutuskan untuk menambahkan satu argumen lagi untuk fungsi murni - dengan fungsi yang disuntikkan secara eksplisit (dipermainkan dalam tes) Anda harus ubah konfigurasi tes, di mana dengan fungsi yang digunakan secara implisit Anda tidak melakukan apa pun.

Saya bisa membayangkan situasi ketika Anda perlu menyuntikkan fungsi murni - adalah ketika Anda ingin menawarkan bagi konsumen untuk mengubah perilaku unit.

public decimal CalculateTotalPrice(Order order, Func<Customer, decimal> calculateDiscount)
{
    // Do some calculation based on order data
    var totalPrice = order.Lines.Sum(line => line.Price);

    // Then
    var discountAmount = calculateDiscount(order.Customer);

    return totalPrice - discountAmount;
}

Untuk metode di atas Anda mengharapkan perhitungan diskon dapat diubah oleh konsumen, jadi Anda harus mengecualikan pengujian logikanya dari unit test untuk CalcualteTotalPricemetode.

Fabio
sumber
Menyuntikkan fungsi tidak berarti Anda harus menguji panggilan fungsi. Sebenarnya saya akan menegaskan bahwa SUT melakukan hal yang benar (mengenai keadaan yang dapat diamati) ketika dilewati tiruan. OTOH mengolok ketergantungan yang bukan bagian dari antarmuka SUT akan aneh di mata saya.
Berhentilah melukai Monica
@ Goyo, Anda akan mengkonfigurasi mock untuk mengembalikan nilai yang diharapkan hanya untuk input tertentu. Jadi tiruan Anda akan "menjaga" argumen input yang benar disediakan.
Fabio
4

Dalam dunia ideal pemrograman SOLID OO Anda akan menyuntikkan setiap perilaku eksternal. Dalam praktiknya Anda akan selalu menggunakan beberapa fungsi sederhana (atau tidak begitu sederhana) secara langsung.

Jika Anda menghitung maksimum satu set angka, mungkin akan terlalu banyak untuk menyuntikkan maxfungsi dan mengejeknya dalam unit test. Biasanya Anda tidak peduli tentang memisahkan kode Anda dari implementasi spesifik maxdan mengujinya secara terpisah.

Jika Anda melakukan sesuatu yang kompleks seperti menemukan jalur biaya minimum dalam grafik maka Anda sebaiknya menyuntikkan implementasi konkret untuk fleksibilitas dan mengejeknya dalam unit test untuk kinerja yang lebih baik. Dalam hal ini mungkin layak karya decoupling algoritma pencarian jalur dari kode yang menggunakannya.

Tidak ada jawaban untuk "fungsi murni", Anda harus memutuskan di mana harus menarik garis. Ada terlalu banyak faktor yang terlibat dalam keputusan ini. Pada akhirnya Anda harus mempertimbangkan manfaat dari masalah yang diberikan decoupling kepada Anda, dan itu tergantung pada Anda dan konteks Anda.

Saya melihat di komentar bahwa Anda bertanya tentang perbedaan antara kelas injeksi (sebenarnya Anda menyuntikkan objek) dan fungsi. Tidak ada perbedaan, hanya fitur bahasa yang membuatnya terlihat berbeda. Dari sudut pandang abstrak memanggil fungsi tidak berbeda dengan memanggil metode beberapa objek dan Anda menyuntikkan (atau tidak) fungsi untuk alasan yang sama Anda menyuntikkan (atau tidak) objek: untuk memisahkan perilaku itu dari kode panggilan dan memiliki kemampuan untuk menggunakan implementasi perilaku yang berbeda dan memutuskan di tempat lain implementasi apa yang digunakan.

Jadi pada dasarnya kriteria apa pun yang Anda anggap valid untuk injeksi dependensi Anda dapat menggunakannya terlepas dari ketergantungannya adalah objek atau fungsi. Mungkin ada beberapa nuansa tergantung pada bahasa (yaitu jika Anda menggunakan java ini bukan masalah).

Berhentilah merugikan Monica
sumber
3
Menyuntikkan tidak ada hubungannya dengan pemrograman berorientasi objek.
Frank Hileman
@ FrankHileman Lebih Baik? Anda memerlukan injeksi ketergantungan untuk bergantung pada abstraksi karena Anda tidak dapat membuat instantiate.
Berhenti menyakiti Monica
Pasti bermanfaat memiliki daftar kasar faktor terpenting yang terlibat dalam praktik. Terkait dengan ini, mengapa Anda tidak peduli tentang penggandengan max()(sebagai lawan dari hal lain)?
DanielM
SOLID bukan "ideal" juga tidak ada hubungannya dengan pemrograman berorientasi objek.
Frank Hileman
@ Frankhileman Bagaimana SOLID tidak ada hubungannya dengan OOP? Lima prinsip tersebut dikemukakan oleh Robert Martin dalam makalahnya tahun 2000 dalam bab berjudul Principles of Object Oriented Class Design?
NickL
3

Fungsi murni tidak memengaruhi testabilitas kelas karena sifat-sifatnya:

  • output mereka (atau kesalahan / pengecualian) hanya tergantung pada input mereka;
  • hasilnya tidak tergantung pada negara dunia;
  • operasi mereka tidak mengubah keadaan dunia.

Ini berarti bahwa mereka secara kasar berada di ranah yang sama dengan metode pribadi, yang sepenuhnya berada di bawah kendali kelas, dengan bonus tambahan bahkan tidak tergantung pada keadaan instance saat ini (yaitu "ini"). Kelas pengguna "mengontrol" output karena sepenuhnya mengontrol input.

Contoh yang Anda berikan, maks () dan min (), bersifat deterministik. Namun, random () dan currentTime () adalah prosedur, bukan fungsi murni (mereka bergantung pada / memodifikasi keadaan dunia keluar dari pita), misalnya. Dua yang terakhir ini harus disuntikkan untuk memungkinkan pengujian.

carlosdavidepto
sumber