Kepada semua yang tidak gentar dalam perjalanan dari penyangkalan hingga keyakinan didedikasikan ...
Ada pendapat yang adil di antara pengembang bahwa jika seorang programmer tidak menutupi kode dengan tes, maka dia sama sekali tidak mengerti mengapa mereka dibutuhkan dan bagaimana mempersiapkannya. Sulit untuk tidak setuju dengan ini ketika Anda sudah mengerti tentang apa itu. Tetapi bagaimana pemahaman yang berharga ini bisa dicapai?
Ini tidak dimaksudkan untuk menjadi ...
Kebetulan sering kali hal yang paling jelas tidak memiliki deskripsi yang jelas di antara banyak informasi berguna di jaringan global. Semacam pelamar biasa memutuskan untuk menangani pertanyaan mendesak "apa tes unit" dan menemukan banyak contoh seperti itu, yang disalin dari artikel ke artikel seperti kertas kalkir:
"Kami memiliki metode yang menghitung jumlah angka" Jumlah bilangan bulat
publik (Bilangan bulat a, Bilangan bulat b) {
return a + b
}
"Anda dapat menulis tes untuk metode ini"
Uji
public void testGoodOne () {
assertThat (jumlah (2,2), adalah (4));
}
Ini bukan lelucon, ini adalah contoh yang disederhanakan dari artikel tipikal tentang teknologi pengujian unit, di mana di awal dan di akhir ada ungkapan umum tentang manfaat dan kebutuhan, dan di tengahnya adalah ...
Melihat ini, dan membacanya ulang dua kali demi keyakinan, pemohon berseru: “Sungguh omong kosong yang kejam ? .. "Lagi pula, dalam kodenya praktis tidak ada metode yang menerima semua yang mereka butuhkan melalui argumen, dan kemudian memberikan hasil yang tidak ambigu untuk mereka. Ini adalah metode utilitarian yang khas dan hampir tidak berubah. Tapi bagaimana dengan prosedur kompleks, dependensi yang diinjeksi, dan metode tanpa mengembalikan nilai? Di sana, pendekatan ini tidak dapat diterapkan dari kata “sama sekali”.
Jika pada tahap ini pemohon yang keras kepala tidak melambaikan tangannya dan menyelam lebih jauh, ia segera menemukan bahwa MOC digunakan untuk dependensi, yang metodenya ditentukan beberapa perilaku bersyarat, sebenarnya sebuah rintisan. Di sini, pemohon benar-benar dapat meledakkan pikirannya jika tidak ada yang baik hati dan sabar menengah / senior yang siap dan mampu menjelaskan semuanya ... Jika tidak, pemohon kebenaran benar-benar kehilangan arti "tes unit apa", karena sebagian besar metode yang diuji ternyata semacam fiksi tiruan , dan apa yang diuji dalam kasus ini tidak jelas. Selain itu, tidak jelas bagaimana mengaturnya untuk aplikasi yang besar dan berlapis-lapis dan mengapa ini diperlukan. Jadi, paling banter, pertanyaan itu ditunda sampai waktu yang lebih baik, paling buruk - ia bersembunyi di dalam sekotak barang terkutuk.
Hal yang paling menjengkelkan adalah bahwa teknologi cakupan pengujian pada dasarnya sederhana dan dapat diakses oleh semua orang, dan manfaatnya sangat jelas sehingga setiap alasan terlihat naif bagi orang yang berpengetahuan. Tetapi untuk mengetahuinya, pemula tidak memiliki esensi dasar yang sangat kecil, seperti tombol flip.
Misi kunci
Untuk memulainya, saya mengusulkan untuk merumuskan secara singkat fungsi kunci (misi) dari pengujian unit dan penguatan kunci. Ada berbagai opsi indah di sini, tetapi saya mengusulkan untuk mempertimbangkan yang satu ini: Fungsi
utama pengujian unit adalah untuk menangkap perilaku yang diharapkan dari sistem.
dan yang ini:
Manfaat utama dari pengujian unit adalah kemampuan untuk "menjalankan" semua fungsionalitas aplikasi dalam hitungan detik.
Saya sarankan mengingat ini untuk wawancara dan akan menjelaskan sedikit. Fungsi apa pun menyiratkan aturan penggunaan dan hasil. Persyaratan ini berasal dari bisnis, melalui analitik sistem, dan diimplementasikan dalam kode. Tetapi kode terus berkembang, persyaratan dan peningkatan baru datang, yang dapat secara tidak disadari dan secara tak terduga mengubah sesuatu dalam fungsionalitas yang sudah selesai. Di sinilah unit tes berdiri berjaga-jaga, yang memperbaiki aturan yang disetujui sesuai dengan mana sistem harus bekerja! Tes mencatat skenario yang penting untuk bisnis, dan jika setelah revisi berikutnya tes gagal, maka ada sesuatu yang hilang: baik pengembang atau analis salah, atau persyaratan baru bertentangan dengan yang sudah ada dan harus diklarifikasi, dll. Yang paling penting adalah "kejutan" itu tidak lolos.
Pengujian unit standar yang sederhana memungkinkan untuk mendeteksi perilaku sistem yang tidak terduga dan mungkin tidak diinginkan sejak dini. Sementara itu, sistem tumbuh dan berkembang, kemungkinan kehilangan detailnya juga meningkat, dan hanya skrip pengujian unit yang dapat mengingat semuanya dan mencegah penyimpangan tepat waktu. Ini sangat nyaman dan dapat diandalkan, dan kenyamanan utamanya adalah kecepatan. Aplikasi bahkan tidak perlu menjalankan dan menjelajahi ratusan bidang, formulir, atau tombolnya, Anda perlu menjalankan pengujian dan mendapatkan kesiapan penuh atau bug dalam hitungan detik.
Jadi, ingat: tangkap perilaku yang diharapkan dalam bentuk skrip pengujian unit, dan langsung "jalankan" aplikasi tanpa meluncurkannya. Ini adalah nilai absolut yang dapat dicapai oleh pengujian unit.
Tapi, sial, bagaimana caranya?
Mari beralih ke bagian yang menyenangkan. Aplikasi modern secara aktif menghilangkan monolititas. Layanan mikro, modul, "lapisan" adalah prinsip dasar pengorganisasian kode kerja, memungkinkan untuk mencapai kemandirian, kemudahan penggunaan kembali, pertukaran dan transfer ke sistem, dll. Layering dan injeksi ketergantungan adalah kunci dalam topik kita.
Pertimbangkan lapisan aplikasi web biasa: pengontrol, layanan, repositori, dll. Selain itu, lapisan utilitas, fasad, model, dan DTO digunakan. Dua yang terakhir tidak boleh mengandung fungsionalitas, mis. metode selain pengakses (pengambil / penyetel), jadi Anda tidak perlu menutupinya dengan pengujian. Kami akan mempertimbangkan sisa lapisan sebagai target cakupan.
Karena perbandingan yang enak ini mungkin tidak disarankan, penerapannya tidak dapat dibandingkan dengan kue puff karena lapisan ini tertanam satu sama lain, seperti dependensi:
- pengontrol mengimplementasikan layanan, yang dipanggil untuk hasilnya
- layanan menyuntikkan repositori (DAO) ke dalam dirinya sendiri, dapat menyuntikkan komponen utilitas
- fasad dirancang untuk menggabungkan pekerjaan banyak layanan atau komponen, masing-masing menyematkannya
Ide utama untuk menguji semua hal ini di seluruh aplikasi: menutupi setiap lapisan secara independen dari lapisan lainnya. Referensi ke kemerdekaan dan fitur anti-monolitik lainnya. Itu. jika repositori diimplementasikan dalam layanan yang diuji, "tamu" ini diejek sebagai bagian dari pengujian layanan, tetapi secara pribadi telah diuji secara jujur sebagai bagian dari pengujian repositori. Dengan demikian, pengujian dibuat untuk setiap elemen dari setiap lapisan, tidak ada yang dilupakan - semuanya ada dalam bisnis.
Prinsip puff pastry
Mari beralih ke contoh, aplikasi sederhana di Java Spring Boot, kodenya akan menjadi dasar, jadi intinya mudah dipahami dan juga berlaku untuk bahasa / kerangka kerja modern lainnya. Aplikasi akan memiliki tugas sederhana - mengalikan angka dengan 3, mis. triple, tetapi pada saat yang sama kami akan membuat aplikasi berlapis-lapis dengan injeksi ketergantungan dan cakupan berlapis dari ujung kepala hingga ujung kaki.
Struktur tersebut berisi paket untuk tiga lapisan: pengontrol, layanan, repo. Struktur tesnya serupa.
Aplikasi akan bekerja seperti ini:
- dari front-end, permintaan GET datang ke pengontrol dengan pengenal nomor yang perlu digandakan.
- pengontrol meminta hasil dari ketergantungan layanannya
- layanan meminta data dari ketergantungannya - repositori, mengalikan dan mengembalikan hasilnya ke pengontrol
- pengontrol melengkapi hasil dan kembali ke front-end
Mari kita mulai dengan pengontrol:
@RestController
@RequiredArgsConstructor
public class SomeController {
private final SomeService someService; // dependency injection
static final String RESP_PREFIX = ": ";
static final String PATH_GET_TRIPLE = "/triple/{numberId}";
@GetMapping(path = PATH_GET_TRIPLE) // mapping method to GET with url=path
public ResponseEntity<String> triple(@PathVariable(name = "numberId") int numberId) {
int res = someService.tripleMethod(numberId); // dependency call
String resp = RESP_PREFIX + res; // own logic
return ResponseEntity.ok().body(resp);
}
}
Pengontrol istirahat tipikal memiliki injeksi ketergantungan someService. Metode tiga dikonfigurasikan untuk permintaan GET ke URL "/ triple / {numberId}", dengan pengenal nomor diteruskan dalam variabel jalur. Metodenya sendiri dapat dibagi menjadi dua komponen utama:
- mengakses ketergantungan - meminta data dari luar, atau memanggil prosedur tanpa hasil
- logika sendiri - bekerja dengan data yang ada
Pertimbangkan layanan:
@Service
@RequiredArgsConstructor
public class SomeService {
private final SomeRepository someRepository; // dependency injection
public int tripleMethod(int numberId) {
Integer fromDB = someRepository.findOne(numberId); // dependency call
int res = fromDB * 3; // own logic
return res;
}
}
Berikut ini situasi yang serupa: memasukkan dependensi someRepository, dan metode tersebut terdiri dari mengakses dependensi dan logikanya sendiri.
Akhirnya - repositori, untuk kesederhanaan, dilakukan tanpa database:
@Repository
public class SomeRepository {
public Integer findOne(Integer id){
return id;
}
}
Metode bersyarat findOne seharusnya mencari database untuk nilai dengan pengenal, tetapi hanya mengembalikan bilangan bulat yang sama. Ini tidak mempengaruhi inti dari teladan kita.
Jika Anda menjalankan aplikasi kami, maka dengan url yang dikonfigurasi Anda dapat melihat:
Bekerja! Berlapis! Dalam produksi ...
Oh ya, tes ...
Sedikit tentang esensinya. Tes menulis juga merupakan proses kreatif! Oleh karena itu, alasan “Saya seorang pengembang, bukan penguji” sama sekali tidak tepat. Tes yang baik, seperti halnya fungsionalitas yang baik, membutuhkan kecerdikan dan keindahan. Tetapi pertama-tama, perlu untuk menentukan struktur dasar tes.
Kelas pengujian berisi metode yang menguji metode kelas target. Minimum yang harus dimiliki setiap metode pengujian adalah panggilan ke metode yang sesuai dari kelas target, secara kondisional berbicara seperti ini:
@Test
void someMethod_test() {
// prepare...
int res = someService.someMethod();
// check...
}
Tantangan ini dapat dikelilingi oleh Persiapan dan Tinjauan. Mempersiapkan data, termasuk argumen input dan mendeskripsikan perilaku para ejekan. Memvalidasi hasil biasanya merupakan perbandingan dengan nilai yang diharapkan, ingat menangkap perilaku yang diharapkan? Secara total, tes adalah skenario yang mensimulasikan situasi dan catatan yang lulus seperti yang diharapkan dan mengembalikan hasil yang diharapkan.
Menggunakan pengontrol sebagai contoh, mari kita coba menggambarkan secara detail algoritme dasar untuk menulis pengujian. Pertama-tama, metode pengontrol target mengambil parameter numberId int, mari tambahkan ke skrip kita:
int numberId = 42; // input path variable
NumberId yang sama ditransmisikan saat transit ke input ke metode layanan, dan sekarang saatnya menyediakan tiruan layanan:
@MockBean
private SomeService someService;
Kode metode pengontrol bekerja dengan hasil yang diterima dari layanan, kami mensimulasikan hasil ini, serta panggilan yang mengembalikannya:
int serviceRes = numberId*3; // result from mock someService
// prepare someService.tripleMethod behavior
when(someService.tripleMethod(eq(numberId))).thenReturn(serviceRes);
Entri ini berarti: "ketika someService.tripleMethod dipanggil dengan argumen yang sama dengan numberId, kembalikan nilai serviceRes."
Selain itu, entri ini menangkap fakta bahwa metode layanan ini harus dipanggil, yang merupakan poin penting. Kebetulan Anda perlu memperbaiki panggilan ke prosedur tanpa hasil, kemudian notasi yang berbeda digunakan, secara konvensional seperti - "tidak melakukan apa-apa saat ...":
Mockito.doNothing().when(someService).someMethod(eq(someParam));
Sekali lagi, ini hanyalah tiruan dari pekerjaan someService, pengujian jujur dengan perbaikan rinci dari perilaku someService akan diimplementasikan secara terpisah. Selain itu, bahkan tidak penting di sini bahwa nilainya harus tiga kali lipat, jika kita menulis
int serviceRes = numberId*5;
ini tidak akan merusak skrip saat ini, karena bukan perilaku someService yang ditangkap di sini, tetapi perilaku controller yang menerima hasil someService begitu saja. Ini sepenuhnya logis, karena kelas target tidak dapat bertanggung jawab atas perilaku dependensi yang diinjeksi, tetapi harus mempercayainya.
Jadi kita telah mendefinisikan perilaku tiruan dalam skrip kita, oleh karena itu, saat menjalankan pengujian, ketika di dalam panggilan ke metode target itu datang ke tiruan, ia akan mengembalikan apa yang diminta - serviceRes, dan kemudian kode pengontrol itu sendiri akan bekerja dengan nilai ini.
Selanjutnya, kami melakukan panggilan ke metode target dalam skrip. Metode pengontrol memiliki kekhasan - metode ini tidak dipanggil secara eksplisit dalam kode, tetapi terikat melalui metode HTTP GET dan URL, oleh karena itu dalam pengujian ini dipanggil melalui klien pengujian khusus. Di Spring, ini adalah MockMvc, di framework lain ada analog, misalnya, WebTestCase.createClient di Symfony. Jadi, selanjutnya, mudah untuk menjalankan metode pengontrol melalui pemetaan dengan GET dan URL.
//// mockMvc.perform
MockHttpServletRequestBuilder requestConfig = MockMvcRequestBuilders.get(SomeController.PATH_GET_TRIPLE, numberId);
MvcResult mvcResult = mockMvc.perform(requestConfig)
.andExpect(status().isOk())
//.andDo(MockMvcResultHandlers.print())
.andReturn()
;//// mockMvc.perform
Pada saat yang sama, juga diperiksa bahwa pemetaan semacam itu ada. Jika panggilan berhasil, itu tergantung pada memeriksa dan memperbaiki hasilnya. Misalnya, Anda dapat memperbaiki berapa kali metode tiruan dipanggil:
// check of calling
Mockito.verify(someService, Mockito.atLeastOnce()).tripleMethod(eq(numberId));
Dalam kasus kami, ini berlebihan, karena kami telah memperbaiki panggilannya hanya ketika, tetapi terkadang metode ini sesuai.
Dan sekarang hal utama - kami memeriksa perilaku kode pengontrol itu sendiri:
// check of result
assertEquals(SomeController.RESP_PREFIX+serviceRes, mvcResult.getResponse().getContentAsString());
Di sini kita telah memperbaiki apa yang menjadi tanggung jawab metode itu sendiri - bahwa hasil yang diterima dari someService digabungkan dengan awalan pengontrol, dan baris inilah yang masuk ke badan respons. Ngomong-ngomong, Anda dapat melihat dengan mata kepala sendiri isi Tubuh jika Anda menghapus komentar pada baris tersebut
//.andDo(MockMvcResultHandlers.print())
tetapi biasanya pencetakan ke konsol ini hanya digunakan sebagai bantuan untuk debugging.
Jadi, kami memiliki metode pengujian di kelas pengujian pengontrol:
@WebMvcTest(SomeController.class)
class SomeControllerTest {
@MockBean
private SomeService someService;
@Autowired
private MockMvc mockMvc;
@Test
void triple() throws Exception {
int numberId = 42; // input path variable
int serviceRes = numberId*3; // result from mock someService
// prepare someService.tripleMethod behavior
when(someService.tripleMethod(eq(numberId))).thenReturn(serviceRes);
//// mockMvc.perform
MockHttpServletRequestBuilder requestConfig = MockMvcRequestBuilders.get(SomeController.PATH_GET_TRIPLE, numberId);
MvcResult mvcResult = mockMvc.perform(requestConfig)
.andExpect(status().isOk())
//.andDo(MockMvcResultHandlers.print())
.andReturn()
;//// mockMvc.perform
// check of calling
Mockito.verify(someService, Mockito.atLeastOnce()).tripleMethod(eq(numberId));
// check of result
assertEquals(SomeController.RESP_PREFIX+serviceRes, mvcResult.getResponse().getContentAsString());
}
}
Sekarang saatnya menguji secara jujur metode someService.tripleMethod, yang juga memiliki panggilan dependensi dan kode Anda sendiri. Siapkan argumen masukan arbitrer dan simulasikan perilaku dependensi someRepository:
int numberId = 42;
when(someRepository.findOne(eq(numberId))).then(AdditionalAnswers.returnsFirstArg());
Terjemahan: "ketika someRepository.findOne dipanggil dengan argumen yang sama dengan numberId, kembalikan argumen yang sama." Situasi serupa - di sini kami tidak memeriksa logika ketergantungan, tetapi kami mengambil kata-katanya untuk itu. Kami hanya menangkap panggilan ke ketergantungan dalam metode ini. Prinsip di sini adalah logika layanan itu sendiri, area tanggung jawabnya:
assertEquals(numberId*3, res);
Kami memperbaiki bahwa nilai yang diterima dari repositori harus digandakan tiga kali lipat oleh logika metode itu sendiri. Sekarang tes ini menjaga persyaratan ini:
@ExtendWith(MockitoExtension.class)
class SomeServiceTest {
@Mock
private SomeRepository someRepository; // ,
@InjectMocks
private SomeService someService; // ,
@Test
void tripleMethod() {
int numberId = 42;
when(someRepository.findOne(eq(numberId))).then(AdditionalAnswers.returnsFirstArg());
int res = someService.tripleMethod(numberId);
assertEquals(numberId*3, res);
}
}
Karena repositori kami adalah mainan bersyarat, maka tesnya ternyata sesuai:
class SomeRepositoryTest {
// no dependency injection
private final SomeRepository someRepository = new SomeRepository();
@Test
void findOne() {
int id = 777;
Integer fromDB = someRepository.findOne(id);
assertEquals(id, fromDB);
}
}
Namun, bahkan di sini seluruh kerangka ada: persiapan, permohonan, dan verifikasi. Dengan demikian, pekerjaan someRepository.findOne yang benar telah diperbaiki.
Repositori sebenarnya memerlukan pengujian dengan menaikkan database dalam memori atau dalam wadah pengujian, memigrasi struktur dan data, terkadang memasukkan catatan pengujian. Ini seringkali merupakan lapisan pengujian terpanjang, tetapi tidak kalah pentingnya karena migrasi yang berhasil, model penyimpanan, pemilihan yang benar, dll. dicatat. Organisasi pengujian database berada di luar cakupan artikel ini, tetapi secara tepat dijelaskan secara rinci dalam manual. Tidak ada injeksi ketergantungan dalam repositori dan tidak diperlukan, tugasnya adalah bekerja dengan database. Dalam kasus kami, ini akan menjadi pengujian dengan penyimpanan awal catatan ke database dan pencarian selanjutnya berdasarkan id.
Dengan demikian, kami telah mencapai cakupan penuh dari seluruh rantai fungsional. Setiap pengujian bertanggung jawab untuk menjalankan kodenya sendiri dan menangkap panggilan ke semua dependensi. Menguji aplikasi tidak memerlukan menjalankannya dengan peningkatan konteks penuh, yang sulit dan memakan waktu. Mempertahankan fungsionalitas dengan pengujian unit yang cepat dan mudah menciptakan lingkungan kerja yang nyaman dan andal.
Selain itu, pengujian meningkatkan kualitas kode. Sebagai bagian dari pengujian independen berlapis, Anda sering kali perlu memikirkan kembali bagaimana Anda mengatur kode Anda. Misalnya, metode telah dibuat di layanan terlebih dahulu, tidak kecil, berisi kode dan tiruannya sendiri, dan, misalnya, tidak masuk akal untuk membaginya, metode ini dicakup oleh pengujian secara penuh - semua persiapan dan pemeriksaan ditentukan. Kemudian seseorang memutuskan untuk menambahkan metode kedua ke layanan tersebut, yang memanggil metode pertama. Tampaknya pernah menjadi situasi umum, tetapi ketika datang ke liputan dengan tes, ada sesuatu yang tidak sesuai ... Untuk metode kedua, Anda harus menjelaskan skenario kedua dan menduplikasi skenario persiapan pertama? Bagaimanapun, itu tidak akan berfungsi untuk mengunci metode pertama dari kelas yang diuji itu sendiri.
Mungkin, dalam kasus ini, adalah tepat untuk memikirkan tentang organisasi kode yang berbeda. Ada dua pendekatan yang berlawanan:
- pindahkan metode pertama ke dalam komponen utilitas yang dimasukkan sebagai dependensi ke dalam layanan.
- pindahkan metode kedua ke dalam fasad layanan yang menggabungkan metode berbeda dari layanan tertanam atau bahkan beberapa layanan.
Kedua opsi ini cocok dengan prinsip "lapisan" dan mudah diuji dengan penghinaan ketergantungan. Keindahannya adalah bahwa setiap lapisan bertanggung jawab atas pekerjaannya sendiri, dan bersama-sama mereka menciptakan kerangka yang kokoh untuk kekebalan seluruh sistem.
Di lintasan ...
Pertanyaan wawancara: Berapa kali pengembang harus menjalankan pengujian dalam satu tiket? Sebanyak yang Anda suka, tetapi setidaknya dua kali:
- sebelum mulai bekerja, untuk memastikan bahwa semuanya baik-baik saja, dan bukan untuk mengetahui nanti apa yang sudah rusak, dan bukan Anda
- di akhir pekerjaan
Jadi mengapa menulis tes? Kemudian, tidak ada gunanya mencoba mengingat dan meramalkan semuanya dalam aplikasi yang besar dan kompleks, itu harus dipercayakan pada otomatisasi. Pengembang yang tidak memiliki pengujian otomatis tidak siap untuk berpartisipasi dalam proyek besar, setiap orang yang diwawancarai akan segera mengungkapkannya.
Oleh karena itu, saya merekomendasikan untuk mengembangkan keterampilan ini jika Anda ingin memenuhi syarat untuk mendapatkan gaji yang tinggi. Anda bisa memulai latihan yang mengasyikkan ini dengan hal-hal dasar, yaitu, dalam kerangka kerangka favorit Anda, pelajari cara menguji:
- komponen dengan dependensi tertanam, teknik mengejek
- pengendali, karena ada nuansa menyebut titik akhir
- DAO, repositori, termasuk meningkatkan basis pengujian dan migrasi
Saya berharap konsep "puff pastry" ini telah membantu untuk memahami teknik pengujian aplikasi yang kompleks dan untuk merasakan betapa fleksibel dan kuatnya alat yang disajikan kepada kita untuk bekerja. Tentu saja, semakin baik alatnya, semakin terampil pekerjaan yang dibutuhkannya.
Nikmati pekerjaan dan keterampilan hebat Anda!
Kode contoh tersedia dari tautan di github.com: https://github.com/denisorlov/examples/tree/main/unittestidea