Pertimbangkan fungsi seperti ini:
function savePeople(dataStore, people) {
people.forEach(person => dataStore.savePerson(person));
}
Ini dapat digunakan seperti ini:
myDataStore = new Store('some connection string', 'password');
myPeople = ['Joe', 'Maggie', 'John'];
savePeople(myDataStore, myPeople);
Mari kita asumsikan bahwa Store
memiliki unit test sendiri, atau disediakan vendor. Bagaimanapun, kami percaya Store
. Dan mari kita asumsikan lebih jauh bahwa penanganan kesalahan - misalnya, kesalahan pemutusan basis data - bukan tanggung jawab savePeople
. Memang, mari kita asumsikan bahwa toko itu sendiri adalah basis data magis yang tidak mungkin kesalahan dengan cara apa pun. Dengan asumsi-asumsi ini, pertanyaannya adalah:
Harus savePeople()
diuji unit, atau apakah tes semacam itu untuk menguji forEach
konstruksi bahasa built-in ?
Kita bisa, tentu saja, berpura-pura dataStore
dan menegaskan bahwa dataStore.savePerson()
itu dipanggil satu kali untuk setiap orang. Anda tentu bisa membuat argumen bahwa tes semacam itu memberikan keamanan terhadap perubahan implementasi: misalnya, jika kami memutuskan untuk mengganti forEach
dengan for
loop tradisional , atau metode iterasi lainnya. Jadi tes ini tidak sepenuhnya sepele. Namun sepertinya sangat dekat ...
Inilah contoh lain yang mungkin lebih bermanfaat. Pertimbangkan fungsi yang tidak melakukan apa pun selain mengoordinasikan objek atau fungsi lain. Sebagai contoh:
function bakeCookies(dough, pan, oven) {
panWithRawCookies = pan.add(dough);
oven.addPan(panWithRawCookies);
oven.bakeCookies();
oven.removePan();
}
Bagaimana seharusnya fungsi seperti ini diuji unit, dengan asumsi Anda pikir seharusnya demikian? Sulit bagi saya untuk membayangkan jenis unit tes yang tidak hanya mengejek dough
, pan
dan oven
, dan kemudian menegaskan bahwa metode disebut pada mereka. Tetapi tes semacam itu tidak lebih dari menduplikasi implementasi fungsi yang tepat.
Apakah ketidakmampuan untuk menguji fungsi dengan cara kotak hitam yang berarti menunjukkan cacat desain dengan fungsi itu sendiri? Jika demikian, bagaimana ini bisa diperbaiki?
Untuk memberikan kejelasan lebih lanjut pada pertanyaan yang memotivasi bakeCookies
contoh, saya akan menambahkan skenario yang lebih realistis, yang saya temui ketika mencoba menambahkan tes ke dan refactor kode warisan.
Ketika seorang pengguna membuat akun baru, sejumlah hal perlu terjadi: 1) catatan pengguna baru perlu dibuat dalam database 2) email selamat datang perlu dikirim 3) alamat IP pengguna perlu direkam untuk penipuan tujuan.
Jadi kami ingin membuat metode yang mengikat semua langkah "pengguna baru":
function createNewUser(validatedUserData, emailService, dataStore) {
userId = dataStore.insertUserRecord(validateduserData);
emailService.sendWelcomeEmail(validatedUserData);
dataStore.recordIpAddress(userId, validatedUserData.ip);
}
Perhatikan bahwa jika salah satu dari metode ini menghasilkan kesalahan, kami ingin kesalahan tersebut muncul hingga ke kode panggilan, sehingga dapat menangani kesalahan sesuai keinginan. Jika dipanggil oleh kode API, itu dapat menerjemahkan kesalahan menjadi kode respons http yang sesuai. Jika itu dipanggil oleh antarmuka web, itu dapat menerjemahkan kesalahan menjadi pesan yang sesuai untuk ditampilkan kepada pengguna, dan sebagainya. Intinya adalah fungsi ini tidak tahu bagaimana menangani kesalahan yang mungkin terjadi.
Inti dari kebingungan saya adalah bahwa untuk menguji unit fungsi seperti itu tampaknya perlu mengulangi implementasi yang tepat dalam tes itu sendiri (dengan menentukan bahwa metode dipanggil pada tiruan dalam urutan tertentu) dan yang tampaknya salah.
sumber
Jawaban:
Haruskah
savePeople()
unit diuji? Iya. Anda tidak menguji yangdataStore.savePerson
berfungsi, atau bahwa koneksi db berfungsi, atau bahkan ituforeach
berhasil. Anda menguji yangsavePeople
memenuhi janji yang dibuatnya melalui kontraknya.Bayangkan skenario ini: seseorang melakukan refactor besar terhadap basis kode, dan secara tidak sengaja menghapus
forEach
bagian dari implementasi sehingga selalu hanya menyimpan item pertama. Tidakkah Anda ingin tes unit menangkap itu?sumber
Biasanya pertanyaan seperti ini muncul ketika orang melakukan pengembangan "test-after". Pendekatan masalah ini dari sudut pandang TDD, di mana tes datang sebelum implementasi, dan tanyakan pada diri sendiri pertanyaan ini lagi sebagai latihan.
Setidaknya dalam aplikasi TDD saya, yang biasanya di luar, saya tidak akan mengimplementasikan fungsi seperti
savePeople
setelah diimplementasikansavePerson
. FungsisavePeople
dansavePerson
akan dimulai sebagai satu dan didorong oleh tes dari unit test yang sama; pemisahan antara keduanya akan timbul setelah beberapa tes, pada langkah refactoring. Mode kerja ini juga akan menimbulkan pertanyaan di mana fungsisavePeople
seharusnya - apakah itu fungsi bebas atau bagian daridataStore
.Pada akhirnya, tes tidak akan hanya memeriksa apakah Anda benar dapat menyimpan
Person
dalamStore
, tetapi juga banyak orang. Ini juga akan membuat saya mempertanyakan apakah pemeriksaan lain diperlukan, misalnya, "Apakah saya perlu memastikan bahwasavePeople
fungsinya adalah atomik, baik menyelamatkan semua atau tidak sama sekali?", "Bisakah itu entah bagaimana mengembalikan kesalahan untuk orang-orang yang tidak bisa t diselamatkan? Seperti apa kesalahan itu? ", dan seterusnya. Semua ini jumlahnya jauh lebih dari sekadar memeriksa penggunaan suatuforEach
atau bentuk iterasi lainnya.Padahal, jika persyaratan untuk menyimpan lebih dari satu orang sekaligus datang hanya setelah
savePerson
sudah dikirimkan, maka saya akan memperbarui tes yang adasavePerson
untuk menjalankan melalui fungsi barusavePeople
, memastikan itu masih dapat menyelamatkan satu orang dengan hanya mendelegasikan pada awalnya, kemudian uji coba perilaku tersebut untuk lebih dari satu orang melalui tes baru, pikirkan apakah perlu untuk membuat perilaku itu atomik atau tidak.sumber
savePeople
? Seperti yang saya jelaskan dalam paragraf terakhir OP atau cara lain?savePerson
fungsi seperti yang Anda sarankan, sebagai gantinya saya akan mengujinya secara lebih umumsavePeople
. Tes unit untukStore
akan diubah untuk dijalankansavePeople
daripada langsung meneleponsavePerson
, jadi untuk ini, tidak ada tiruan yang digunakan. Tetapi database tentu saja tidak boleh ada, karena kami ingin mengisolasi masalah pengkodean dari berbagai masalah integrasi yang terjadi dengan database aktual, jadi di sini kami masih memiliki tiruan.Ya, seharusnya begitu. Tetapi cobalah untuk menulis kondisi pengujian Anda dengan cara yang independen dari implementasi. Misalnya, mengubah contoh penggunaan Anda menjadi unit test:
Tes ini melakukan banyak hal:
savePeople()
savePeople()
savePeople()
Perhatikan bahwa Anda masih dapat mengejek / mematikan / memalsukan penyimpanan data. Dalam hal ini saya tidak akan memeriksa panggilan fungsi eksplisit, tetapi untuk hasil operasi. Dengan cara ini pengujian saya disiapkan untuk perubahan / refaktor di masa depan.
Misalnya, implementasi penyimpanan data Anda mungkin menyediakan
saveBulkPerson()
metode di masa mendatang - sekarang perubahan pada penerapansavePeople()
untuk menggunakansaveBulkPerson()
tidak akan merusak pengujian unit selamasaveBulkPerson()
pekerjaan seperti yang diharapkan. Dan jikasaveBulkPerson()
entah bagaimana tidak berfungsi seperti yang diharapkan, unit test Anda akan menangkapnya.Seperti yang dikatakan, cobalah untuk menguji hasil yang diharapkan dan antarmuka fungsi, bukan untuk implementasi (kecuali jika Anda melakukan tes integrasi - maka menangkap panggilan fungsi tertentu mungkin berguna). Jika ada beberapa cara untuk mengimplementasikan suatu fungsi, semuanya harus bekerja dengan unit test Anda.
Mengenai pembaruan pertanyaan Anda:
Uji perubahan negara! Misal beberapa adonan akan digunakan. Menurut implementasi Anda, nyatakan bahwa jumlah yang digunakan
dough
sesuaipan
atau menyatakan bahwadough
habis. Menyatakan bahwapan
cookie berisi setelah fungsi panggilan. Menyatakan bahwaoven
kosong / dalam keadaan yang sama seperti sebelumnya.Untuk tes tambahan, verifikasi kasus tepi: Apa yang terjadi jika
oven
tidak kosong sebelum panggilan? Apa yang terjadi jika tidak cukupdough
? Jikapan
sudah penuh?Anda harus dapat menyimpulkan semua data yang diperlukan untuk pengujian ini dari objek adonan, penggorengan dan oven itu sendiri. Tidak perlu menangkap panggilan fungsi. Perlakukan fungsi seolah-olah implementasinya tidak akan tersedia untuk Anda!
Bahkan, sebagian besar pengguna TDD menulis tes mereka sebelum mereka menulis fungsi sehingga mereka tidak bergantung pada implementasi yang sebenarnya.
Untuk tambahan terbaru Anda:
Untuk fungsi seperti ini saya akan mengejek / rintisan / palsu (apa pun yang tampak lebih umum)
dataStore
danemailService
parameter. Fungsi ini tidak melakukan transisi status apa pun pada parameter apa pun, ia mendelegasikannya ke metode beberapa di antaranya. Saya akan mencoba memverifikasi bahwa panggilan ke fungsi melakukan 4 hal:3 pemeriksaan pertama dapat dilakukan dengan mengejek, bertopik atau palsu
dataStore
danemailService
(Anda benar-benar tidak ingin mengirim email saat pengujian). Karena saya harus mencari ini untuk beberapa komentar, inilah perbedaannya:dataStore
itu hanya mengimplementasikan versiinsertUserRecord()
danrecordIpAddress()
.Pengecualian / kesalahan yang diharapkan adalah kasus uji yang valid: Anda mengonfirmasi, bahwa, dalam hal peristiwa seperti itu terjadi, fungsi berperilaku seperti yang Anda harapkan. Ini dapat dicapai dengan membiarkan benda tiruan / tiruan / rintisan yang sesuai melempar saat diinginkan.
Kadang-kadang ini harus dilakukan (meskipun Anda lebih peduli tentang ini dalam tes integrasi). Lebih sering, ada cara lain untuk memverifikasi efek samping yang diharapkan / perubahan keadaan.
Memverifikasi panggilan fungsi yang tepat menghasilkan tes unit yang agak rapuh: Hanya perubahan kecil pada fungsi asli yang menyebabkannya gagal. Ini bisa diinginkan atau tidak, tetapi itu memerlukan perubahan pada unit test yang sesuai setiap kali Anda mengubah suatu fungsi (baik itu refactoring, optimalisasi, perbaikan bug, ...).
Sayangnya, dalam kasus itu, unit test kehilangan beberapa kredibilitasnya: karena itu diubah, itu tidak mengkonfirmasi fungsi setelah perubahan berperilaku dengan cara yang sama seperti sebelumnya.
Sebagai contoh, pertimbangkan seseorang menambahkan panggilan ke
oven.preheat()
(pengoptimalan!) Dalam contoh pembuatan cookie Anda:Dalam pengujian unit saya, saya mencoba untuk menjadi se-umum mungkin: Jika implementasi berubah, tetapi perilaku yang terlihat (dari perspektif pemanggil) masih sama, tes saya harus lulus. Idealnya, satu-satunya kasus saya perlu mengubah unit test yang ada harus berupa perbaikan bug (dari pengujian, bukan fungsi yang sedang diuji).
sumber
myDataStore.containsPerson('Joe')
Anda mengasumsikan adanya database uji fungsional. Setelah Anda melakukannya, Anda menulis tes integrasi dan bukan tes unit.savePeople()
benar - benar menambahkan orang-orang ke penyimpanan data apa pun yang Anda berikan selama penyimpanan data mengimplementasikan antarmuka yang diharapkan. Sebagai contoh, tes integrasi akan memeriksa apakah pembungkus basis data saya benar-benar melakukan pemanggilan basis data yang tepat untuk pemanggilan metode.myDataStore.containsPerson('Joe')
, Anda harus menggunakan db fungsional dari beberapa jenis. Setelah Anda mengambil langkah itu bukan lagi tes unit.myPeople
) ada di dalam array. IMHO mock masih harus memiliki perilaku yang dapat diamati sama yang diharapkan dari objek nyata, jika tidak Anda menguji kepatuhan dengan antarmuka tiruan, bukan antarmuka nyata.Nilai utama yang disediakan oleh tes tersebut adalah bahwa hal itu membuat implementasi Anda menjadi reaktif.
Saya biasa melakukan banyak optimasi kinerja dalam karir saya dan sering menemukan masalah dengan pola yang tepat yang Anda tunjukkan: untuk menyimpan entitas N ke dalam database, melakukan insert N. Biasanya lebih efisien untuk melakukan penyisipan massal menggunakan pernyataan tunggal.
Di sisi lain, kami juga tidak ingin mengoptimalkan secara prematur. Jika Anda biasanya hanya menghemat 1 - 3 orang sekaligus, maka menulis batch yang dioptimalkan mungkin berlebihan.
Dengan unit test yang tepat, Anda dapat menuliskannya dengan cara Anda menerapkannya di atas, dan jika Anda perlu mengoptimalkannya, Anda bebas melakukannya dengan jaring pengaman dari tes otomatis untuk menangkap kesalahan. Secara alami, ini bervariasi berdasarkan kualitas tes, jadi uji secara bebas dan uji dengan baik.
Keuntungan sekunder dari unit yang menguji perilaku ini adalah untuk berfungsi sebagai dokumentasi untuk apa tujuannya. Contoh sepele ini mungkin jelas, tetapi mengingat poin berikutnya di bawah ini, itu bisa sangat penting.
Keuntungan ketiga, yang telah ditunjukkan oleh orang lain, adalah Anda dapat menguji rincian di bawah-penutup yang sangat sulit untuk diuji dengan tes integrasi atau penerimaan. Sebagai contoh, jika ada persyaratan bahwa semua pengguna disimpan secara atom, maka Anda dapat menulis sebuah test case untuk itu, yang memberi Anda cara untuk mengetahui berperilaku seperti yang diharapkan, dan juga berfungsi sebagai dokumentasi untuk suatu persyaratan yang mungkin tidak jelas. untuk pengembang baru.
Saya akan menambahkan pemikiran yang saya dapatkan dari instruktur TDD. Jangan menguji metodenya. Uji perilakunya. Dengan kata lain, Anda tidak menguji yang
savePeople
berfungsi, Anda menguji bahwa banyak pengguna dapat disimpan dalam satu panggilan.Saya menemukan kemampuan saya untuk melakukan pengujian unit kualitas dan TDD membaik ketika saya berhenti berpikir tentang pengujian unit sebagai memverifikasi bahwa suatu program berfungsi, tetapi, mereka memverifikasi bahwa unit kode melakukan apa yang saya harapkan . Itu berbeda. Mereka tidak memverifikasi itu berfungsi, tetapi mereka memverifikasi itu melakukan apa yang saya pikir tidak. Ketika saya mulai berpikir seperti itu, perspektif saya berubah.
sumber
savePerson
memanggilnya untuk setiap orang dalam daftar - akan pecah dengan refactoring memasukkan massal, namun. Yang bagi saya menunjukkan bahwa ini adalah tes unit yang buruk. Namun, saya tidak melihat alternatif yang akan melewati implementasi curah dan satu-save-per-orang, tanpa menggunakan database pengujian yang sebenarnya dan menyatakan menentangnya, yang tampaknya salah. Bisakah Anda memberikan tes yang berfungsi untuk kedua implementasi?savePerson
metode dipanggil untuk setiap input, dan jika Anda mengganti loop dengan insert massal, Anda tidak akan memanggil metode itu lagi. Jadi tes Anda akan pecah. Jika Anda memiliki hal lain dalam pikiran, saya terbuka untuk itu, tetapi saya belum melihatnya. (Dan tidak melihat maksud saya.)Harus
bakeCookies()
diuji? Iya.Tidak juga. Perhatikan dengan seksama pada APA fungsi yang seharusnya dilakukan - itu seharusnya mengatur
oven
objek ke keadaan tertentu. Melihat kode itu tampak bahwa keadaanpan
dandough
objek tidak terlalu penting. Jadi, Anda harus melewati sebuahoven
objek (atau mengejeknya) dan menyatakan bahwa ia dalam keadaan tertentu di akhir pemanggilan fungsi.Dengan kata lain, Anda harus menyatakan bahwa
bakeCookies()
kue itu dipanggang .Untuk fungsi yang sangat singkat, tes unit mungkin tampak sedikit lebih dari tautologi. Tapi jangan lupa, program Anda akan bertahan jauh lebih lama daripada waktu Anda dipekerjakan menulisnya. Fungsi itu mungkin atau mungkin tidak berubah di masa depan.
Tes unit melayani dua fungsi:
Ini menguji bahwa semuanya berfungsi. Ini adalah tes unit fungsi yang paling tidak berguna dan tampaknya Anda hanya mempertimbangkan fungsionalitas ini ketika mengajukan pertanyaan.
Ia memeriksa untuk melihat bahwa modifikasi program di masa depan tidak merusak fungsi yang sebelumnya diterapkan. Ini adalah fungsi yang paling berguna dari unit test dan mencegah masuknya bug ke dalam program besar. Ini berguna dalam pengkodean normal ketika menambahkan fitur ke program tetapi lebih berguna dalam refactoring dan optimisasi di mana algoritma inti yang mengimplementasikan program diubah secara dramatis tanpa mengubah perilaku program yang dapat diamati.
Jangan menguji kode di dalam fungsi. Alih-alih menguji bahwa fungsi melakukan apa yang dikatakannya. Ketika Anda melihat unit test dengan cara ini (fungsi pengujian, bukan kode) maka Anda akan menyadari bahwa Anda tidak pernah menguji konstruksi bahasa atau bahkan logika aplikasi. Anda sedang menguji API.
sumber
bakeCookies
diuji dengan cara ini, mereka cenderung rusak selama refaktor yang tidak akan memengaruhi perilaku aplikasi yang dapat diamati.Iya. Tetapi Anda bisa melakukannya dengan cara yang hanya akan menguji ulang konstruk.
Hal yang perlu diperhatikan di sini adalah bagaimana fungsi ini berperilaku ketika
savePerson
gagal setengah jalan? Bagaimana cara kerjanya?Itu adalah semacam perilaku halus yang disediakan fungsi yang harus Anda laksanakan dengan tes unit.
sumber
savePeople
tidak boleh bertanggung jawab atas penanganan kesalahan. Untuk mengklarifikasi lagi, dengan asumsi bahwa hanyasavePeople
bertanggung jawab untuk iterasi melalui daftar dan mendelegasikan penyimpanan setiap item ke metode lain, haruskah itu masih diuji?foreach
konstruksi, dan bukan kondisi, efek samping atau perilaku di luarnya, maka Anda benar; unit test baru benar-benar tidak terlalu menarik.Kuncinya di sini adalah perspektif Anda tentang fungsi tertentu sebagai hal sepele. Sebagian besar pemrograman adalah sepele: tetapkan nilai, lakukan beberapa matematika, buat keputusan: jika ini maka itu, lanjutkan perulangan sampai ... Dalam isolasi, semuanya sepele. Anda baru saja membaca 5 bab pertama dari setiap buku yang mengajarkan bahasa pemrograman.
Fakta bahwa menulis tes sangat mudah harus menjadi pertanda bahwa desain Anda tidak terlalu buruk. Apakah Anda lebih suka desain yang tidak mudah diuji?
"Itu tidak akan pernah berubah." adalah bagaimana proyek yang paling gagal dimulai. Tes unit hanya menentukan apakah unit berfungsi seperti yang diharapkan dalam serangkaian keadaan tertentu. Dapatkan untuk lulus dan kemudian Anda bisa melupakan detail implementasi dan hanya menggunakannya. Gunakan ruang otak itu untuk tugas selanjutnya.
Mengetahui segala sesuatunya berjalan seperti yang diharapkan sangat penting dan tidak sepele dalam proyek besar dan terutama tim besar. Jika ada satu kesamaan yang dimiliki programmer, adalah fakta bahwa kita semua harus berurusan dengan kode buruk orang lain. Paling tidak yang bisa kita lakukan adalah melakukan beberapa tes. Jika ragu, tulis tes dan lanjutkan.
sumber
Ini sudah dijawab oleh @BryanOakley, tapi saya punya beberapa argumen tambahan (saya kira):
Pertama uji unit adalah untuk menguji pemenuhan kontrak, bukan implementasi API; tes harus menetapkan prasyarat kemudian memanggil, kemudian memeriksa efek, efek samping, setiap invarian dan kondisi posting. Saat Anda memutuskan apa yang akan diuji, implementasi API tidak masalah (dan tidak seharusnya) .
Kedua, tes Anda akan ada di sana untuk memeriksa invarian ketika fungsi berubah . Fakta itu tidak berubah sekarang tidak berarti Anda tidak harus mengikuti tes.
Ketiga, ada nilai dalam menerapkan tes sepele, baik dalam pendekatan TDD (yang mengamanatkannya) dan di luar itu.
Saat menulis C ++, untuk kelas saya, saya cenderung menulis tes sepele yang instantiate objek dan memeriksa invarian (ditugaskan, reguler, dll). Saya menemukan itu mengejutkan berapa kali tes ini rusak selama pengembangan (dengan - misalnya - menambahkan anggota yang tidak bergerak di kelas, karena kesalahan).
sumber
Saya pikir pertanyaan Anda intinya adalah:
Bagaimana cara unit menguji fungsi kosong tanpa menjadi tes integrasi?
Jika kami mengubah fungsi memanggang cookie Anda untuk mengembalikan cookie misalnya, segera menjadi jelas apa tes yang seharusnya.
Jika kita harus memanggil pan. Dapatkan Cookies setelah memanggil fungsi meskipun kita dapat mempertanyakan apakah ini benar-benar sebuah uji integrasi atau bukan, tetapi apakah kita hanya menguji objek pan?
Saya pikir Anda benar karena memiliki unit test dengan segala yang diejek dan hanya memeriksa fungsi xy dan z disebut nilai kurang.
Tapi! Saya berpendapat bahwa dalam kasus ini Anda harus memperbaiki fungsi void Anda untuk mengembalikan hasil yang dapat diuji ATAU menggunakan objek nyata dan membuat tes integrasi
--- Perbarui untuk contoh createNewUser
OK jadi kali ini hasil fungsi tidak mudah dikembalikan. Kami ingin mengubah status parameter.
Di sinilah saya mendapatkan sedikit kontroversial. Saya membuat implementasi tiruan konkret untuk parameter stateful
tolong, para pembaca yang budiman, cobalah dan kendalikan amarahmu!
begitu...
Ini memisahkan detail implementasi dari metode yang diuji dari perilaku yang diinginkan. Implementasi alternatif:
Masih akan lulus uji unit. Plus Anda memiliki keuntungan untuk dapat menggunakan kembali benda-benda tiruan di seluruh tes dan juga menyuntikkannya ke dalam aplikasi Anda untuk UI atau Tes Integrasi.
sumber
bakeCookies
mengembalikan cookie yang dipanggang tepat, dan saya punya beberapa pemikiran setelah mempostingnya. Jadi saya pikir itu lagi bukan contoh yang bagus. Saya menambahkan satu lagi pembaruan yang semoga memberikan contoh yang lebih realistis tentang apa yang memotivasi pertanyaan saya. Saya menghargai masukan Anda.Anda juga harus menguji
bakeCookies
- apa yang akan / harus Anda hasilkanbakeCookies(egg, pan, oven)
? Telur goreng atau pengecualian? Sendiri, tidakpan
juga tidakoven
akan peduli tentang bahan-bahan yang sebenarnya, karena tidak satupun dari mereka yang seharusnya, tetapibakeCookies
harus biasanya menghasilkan cookies. Lebih umum itu dapat bergantung pada bagaimanadough
diperoleh dan apakah ada adalah kesempatan itu menjadi belakaegg
atau misalnyawater
sebagai gantinya.sumber