
Datang ke proyek baru, saya secara teratur menemukan salah satu situasi berikut:
- Tidak ada tes sama sekali.
- Ada beberapa tes, mereka jarang ditulis dan tidak berjalan secara berkelanjutan.
- Tes hadir dan termasuk dalam CI (Continuous Integration), tetapi lebih banyak ruginya daripada kebaikan.
Sayangnya, ini adalah skenario terakhir yang sering mengarah pada upaya serius untuk mulai melaksanakan pengujian tanpa adanya keterampilan yang sesuai.
Apa yang bisa dilakukan untuk mengubah situasi saat ini? Gagasan menggunakan tes bukanlah hal baru. Pada saat yang sama, sebagian besar tutorial menyerupai gambar terkenal tentang cara menggambar burung hantu: kita menghubungkan JUnit, menulis tes pertama, gunakan mock pertama dan pergi! Artikel semacam itu tidak menjawab pertanyaan tentang tes apa yang perlu ditulis, apa yang perlu diperhatikan dan bagaimana menjalani semua ini. Dari sinilah ide artikel ini lahir. Saya mencoba merangkum pengalaman saya secara singkat dalam mengimplementasikan tes di berbagai proyek untuk memfasilitasi jalur ini untuk semua orang.

Ada lebih dari cukup artikel pengantar tentang topik ini, jadi kami tidak akan mengulangi dan mencoba untuk pergi dari sisi lain. Pada bagian pertama, kami akan menyanggah mitos bahwa pengujian membawa biaya tambahan secara eksklusif. Akan ditunjukkan bagaimana pembuatan tes kualitas dapat mempercepat proses pengembangan. Kemudian, pada contoh proyek kecil, prinsip dan aturan dasar yang harus diikuti untuk mewujudkan manfaat ini akan dipertimbangkan. Akhirnya, di bagian akhir, rekomendasi implementasi spesifik akan diberikan: bagaimana menghindari masalah yang khas ketika tes dimulai, sebaliknya, secara signifikan memperlambat pengembangan.
Karena spesialisasi utama saya adalah Java backend, tumpukan teknologi berikut akan digunakan dalam contoh: Java, JUnit, H2, Mockito, Spring, Hibernate. Pada saat yang sama, bagian penting dari artikel ini dikhususkan untuk masalah pengujian umum dan tips di dalamnya berlaku untuk berbagai tugas yang jauh lebih luas.
Namun berhati-hatilah! Tes sangat adiktif: sekali Anda belajar cara menggunakannya, Anda tidak bisa lagi hidup tanpanya.
Tes vs kecepatan pengembangan
Pertanyaan utama yang muncul ketika membahas implementasi pengujian: berapa lama waktu yang dibutuhkan untuk menulis tes dan apa manfaatnya? Pengujian, seperti teknologi lainnya, akan membutuhkan upaya serius untuk pengembangan dan implementasi, jadi pada awalnya tidak ada manfaat signifikan yang diharapkan. Adapun biaya waktu, mereka sangat tergantung pada tim tertentu. Namun, kurang dari 20-30% dari biaya tambahan pengkodean tidak harus dihitung secara tepat. Kurang sama sekali tidak cukup untuk mencapai setidaknya beberapa hasil. Harapan pengembalian instan sering menjadi alasan utama untuk membatasi kegiatan ini bahkan sebelum tes menjadi berguna.
Tetapi efisiensi seperti apa yang sedang kita bicarakan? Mari kita letakkan lirik tentang kesulitan implementasi dan lihat peluang spesifik apa yang dapat digunakan untuk menghemat waktu pengujian.
Menjalankan kode di sembarang tempat
Jika tidak ada tes dalam proyek, satu-satunya cara untuk memulai adalah mengangkat seluruh aplikasi. Ini bagus jika dibutuhkan sekitar 15-20 detik, tetapi kasus proyek besar di mana peluncuran penuh dapat berlangsung dari beberapa menit jauh dari langka. Apa artinya ini bagi pengembang? Bagian penting dari waktu kerja mereka adalah sesi tunggu singkat ini, di mana Anda tidak dapat terus mengerjakan tugas saat ini, tetapi pada saat yang sama ada terlalu sedikit waktu untuk beralih ke hal lain. Banyak setidaknya sekali mengalami proyek-proyek seperti itu di mana kode yang ditulis dalam satu jam membutuhkan berjam-jam debug karena restart lama antara koreksi. Dalam pengujian, Anda dapat membatasi diri untuk menjalankan bagian kecil dari aplikasi, yang secara signifikan akan mengurangi waktu tunggu dan meningkatkan produktivitas mengerjakan kode.
Selain itu, kemampuan untuk menjalankan kode di sembarang tempat mengarah pada debugging yang lebih menyeluruh. Sering memeriksa bahkan kasus penggunaan positif utama melalui antarmuka aplikasi membutuhkan upaya dan waktu yang serius. Kehadiran tes memungkinkan untuk melakukan pemeriksaan terperinci dari fungsional tertentu yang jauh lebih mudah dan lebih cepat.
Kelebihan lainnya adalah kemampuan untuk mengatur ukuran unit yang diuji. Bergantung pada kompleksitas logika yang diuji, Anda dapat membatasi diri pada satu metode, kelas, sekelompok kelas yang menerapkan beberapa fungsi, layanan, dan sebagainya, hingga otomatisasi pengujian seluruh aplikasi. Fleksibilitas ini memungkinkan Anda untuk memuat tes tingkat tinggi dari banyak bagian karena fakta bahwa tes tersebut akan diuji pada tingkat yang lebih rendah.
Meluncurkan kembali Tes
Nilai tambah ini sering disebut sebagai inti dari otomatisasi pengujian, tetapi mari kita lihat dari sudut yang kurang familiar. Peluang baru apa yang terbuka untuk pengembang?
Pertama, setiap pengembang baru yang datang ke proyek akan dapat dengan mudah menjalankan tes yang ada untuk memahami logika aplikasi menggunakan contoh. Sayangnya, pentingnya ini sangat diremehkan. Dalam kondisi modern, orang yang sama jarang mengerjakan proyek lebih dari 1-2 tahun. Dan karena tim terdiri dari beberapa orang, penampilan peserta baru setiap 2-3 bulan adalah situasi khas untuk proyek yang relatif besar. Terutama proyek-proyek yang sulit sedang mengalami perubahan dari seluruh generasi pengembang! Kemampuan untuk dengan mudah meluncurkan bagian mana pun dari aplikasi dan melihat perilaku sistem pada waktu menyederhanakan perendaman programmer baru dalam proyek tersebut. Selain itu, studi yang lebih rinci tentang logika kode mengurangi jumlah kesalahan yang dibuat pada output dan waktu untuk men-debug mereka di masa depan.
Kedua, kemampuan untuk dengan mudah memverifikasi bahwa aplikasi bekerja dengan benar membuka jalan untuk Refactoring Berkelanjutan. Istilah ini, sayangnya, jauh kurang populer daripada CI. Ini berarti bahwa refactoring dapat dan harus dilakukan setiap kali kode disempurnakan. Justru ketaatan reguler dari aturan kepanduan yang terkenal "meninggalkan tempat parkir lebih bersih daripada sebelum kedatangan Anda" yang memungkinkan Anda untuk menghindari degradasi basis kode dan menjamin proyek ini hidup yang panjang dan bahagia.
Debugging
Debugging telah disebutkan dalam paragraf sebelumnya, tetapi poin ini sangat penting sehingga layak untuk dilihat lebih dekat. Sayangnya, tidak ada cara yang dapat diandalkan untuk mengukur hubungan antara waktu yang dihabiskan untuk menulis kode dan men-debug-nya, karena proses ini praktis tidak dapat dipisahkan satu sama lain. Namun demikian, keberadaan tes kualitas dalam proyek secara signifikan mengurangi waktu debugging, hingga hampir tidak ada kebutuhan untuk menjalankan debugger.
Keefektifan
Semua hal di atas dapat secara signifikan menghemat waktu pada debugging awal kode. Dengan pendekatan yang tepat, hanya ini yang akan membayar semua biaya pengembangan tambahan. Bonus pengujian yang tersisa - meningkatkan kualitas basis kode (kode yang dirancang dengan buruk sulit untuk diuji), mengurangi jumlah cacat, kemampuan untuk memverifikasi kebenaran kode kapan saja, dll. - akan mendapatkan hampir gratis.
Dari teori ke praktik
Dengan kata-kata, semuanya terlihat bagus, tetapi mari kita beralih ke bisnis. Seperti disebutkan sebelumnya, ada lebih dari cukup bahan tentang cara membuat pengaturan awal lingkungan pengujian. Karena itu, kami segera melanjutkan ke proyek yang sudah selesai.
Sumber di sini.Tantangan
Sebagai tugas templat, pertimbangkan fragmen kecil di bagian belakang toko online. Kami akan menulis API khas untuk bekerja dengan produk: membuat, menerima, mengedit. Serta beberapa metode untuk bekerja dengan klien: mengubah "produk favorit" dan menghitung poin bonus untuk pesanan.
Model domain
Agar tidak membebani contoh, kami membatasi diri pada set minimal bidang dan kelas.
Pelanggan memiliki nama pengguna, tautan ke produk favorit dan bendera yang menunjukkan apakah ia adalah pelanggan premium.
Produk (Produk) - nama, harga, diskon, dan bendera yang menunjukkan apakah saat ini diiklankan.
Struktur proyek
Struktur kode proyek utama adalah sebagai berikut.
Kelas berlapis:
- Model - model domain dari proyek;
- Jpa - repositori untuk bekerja dengan database berdasarkan Data Spring;
- Layanan - logika bisnis dari aplikasi;
- Controller - pengendali yang mengimplementasikan API.
Struktur tes unit.
Kelas uji berada dalam paket yang sama dengan kode asli. Selain itu, paket dengan pembangun untuk persiapan data uji telah dibuat, tetapi lebih lanjut tentang itu di bawah ini.
Lebih mudah untuk memisahkan tes unit dan tes integrasi. Mereka sering memiliki ketergantungan yang berbeda, dan untuk perkembangan yang nyaman harus ada kemampuan untuk menjalankan salah satu dari yang lain. Ini dapat dicapai dengan berbagai cara: konvensi penamaan, modul, paket, sourceSet. Pilihan metode tertentu hanya masalah selera. Dalam proyek ini, tes integrasi terletak pada sourceSet yang terpisah - IntegrationTest.
Seperti halnya tes unit, kelas dengan tes integrasi berada dalam paket yang sama dengan kode asli. Selain itu, ada kelas dasar yang membantu menghilangkan duplikasi konfigurasi dan, jika perlu, berisi metode universal yang bermanfaat.
Tes integrasi
Ada beberapa pendekatan berbeda untuk menguji apa yang layak dimulai. Jika logika yang diuji tidak terlalu rumit, Anda dapat langsung melanjutkan ke yang integrasi (kadang-kadang disebut juga yang menerima). Tidak seperti unit test, mereka memastikan bahwa aplikasi secara keseluruhan berfungsi dengan benar.
ArsitekturPertama, Anda perlu memutuskan pada pemeriksaan integrasi level spesifik apa yang akan dilakukan. Spring Boot memberikan kebebasan penuh untuk memilih: Anda dapat meningkatkan bagian dari konteks, seluruh konteks, dan bahkan server penuh, dapat diakses dari tes. Ketika ukuran aplikasi meningkat, masalah ini menjadi semakin kompleks. Seringkali Anda harus menulis tes yang berbeda di level yang berbeda.
Titik awal yang baik adalah tes pengontrol tanpa memulai server. Dalam aplikasi yang relatif kecil, cukup diterima untuk meningkatkan seluruh konteks, karena secara default itu digunakan kembali antara tes dan diinisialisasi hanya sekali. Pertimbangkan metode dasar kelas
ProductController
:
@PostMapping("new") public Product createProduct(@RequestBody Product product) { return productService.createProduct(product); } @GetMapping("{productId}") public Product getProduct(@PathVariable("productId") long productId) { return productService.getProduct(productId); } @PostMapping("{productId}/edit") public void updateProduct(@PathVariable("productId") long productId, @RequestBody Product product) { productService.updateProduct(productId, product); }
Masalah penanganan kesalahan dikesampingkan. Misalkan itu diterapkan secara eksternal berdasarkan analisis pengecualian yang dilemparkan. Kode metode ini sangat sederhana, penerapannya di
ProductService
tidak jauh lebih rumit:
@Transactional(readOnly = true) public Product getProduct(Long productId) { return productRepository.findById(productId) .orElseThrow(() -> new DataNotFoundException("Product", productId)); } @Transactional public Product createProduct(Product product) { return productRepository.save(new Product(product)); } @Transactional public Product updateProduct(Long productId, Product product) { Product dbProduct = productRepository.findById(productId) .orElseThrow(() -> new DataNotFoundException("Product", productId)); dbProduct.setPrice(product.getPrice()); dbProduct.setDiscount(product.getDiscount()); dbProduct.setName(product.getName()); dbProduct.setIsAdvertised(product.isAdvertised()); return productRepository.save(dbProduct); }
Repositori
ProductRepository
tidak mengandung metode sendiri sama sekali:
public interface ProductRepository extends JpaRepository<Product, Long> { }
Semuanya mengisyaratkan bahwa kelas-kelas ini tidak memerlukan tes unit hanya karena seluruh rantai dapat dengan mudah dan efisien diperiksa oleh beberapa tes integrasi. Duplikasi tes yang sama dalam tes yang berbeda mempersulit proses debug. Dalam hal terjadi kesalahan dalam kode, sekarang tidak satu tes akan jatuh, tetapi 10-15 sekaligus. Ini pada gilirannya akan membutuhkan analisis lebih lanjut. Jika tidak ada duplikasi, maka satu-satunya tes jatuh cenderung segera menunjukkan kesalahan.
KonfigurasiUntuk kenyamanan, kami menyoroti kelas dasar
BaseControllerIT
, yang berisi konfigurasi Spring dan beberapa bidang:
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @Transactional public abstract class BaseControllerIT { @Autowired protected ProductRepository productRepository; @Autowired protected CustomerRepository customerRepository; }
Repositori dipindahkan ke kelas dasar agar tidak mengacaukan kelas tes. Peran mereka adalah bantu tambahan: menyiapkan data dan memeriksa status database setelah pengontrol bekerja. Ketika Anda meningkatkan ukuran aplikasi, ini mungkin tidak lagi nyaman, tetapi untuk awalnya sangat cocok.
Konfigurasi utama Spring didefinisikan oleh baris berikut:
@SpringBootTest
- digunakan untuk mengatur konteks aplikasi.
WebEnvironment.NONE
berarti tidak ada konteks web yang perlu ditingkatkan.
@Transactional
- membungkus semua tes kelas dalam suatu transaksi dengan rollback otomatis untuk menyimpan status basis data.
Struktur ujiMari kita beralih ke serangkaian tes minimalis untuk kelas
ProductControllerIT
-
ProductControllerIT
.
@Test public void createProduct_productSaved() { Product product = product("productName").price("1.01").discount("0.1").advertised(true).build(); Product createdProduct = productController.createProduct(product); Product dbProduct = productRepository.getOne(createdProduct.getId()); assertEquals("productName", dbProduct.getName()); assertEquals(number("1.01"), dbProduct.getPrice()); assertEquals(number("0.1"), dbProduct.getDiscount()); assertEquals(true, dbProduct.isAdvertised()); }
Sekilas kode tes harus sangat sederhana dan mudah dimengerti. Jika tidak demikian, maka sebagian besar keuntungan dari tes yang dijelaskan pada bagian pertama artikel hilang. Merupakan praktik yang baik untuk membagi badan uji menjadi tiga bagian yang dapat dipisahkan secara visual satu sama lain: menyiapkan data, memanggil metode pengujian, memvalidasi hasil. Pada saat yang sama, sangat diinginkan agar kode uji sesuai pada layar secara keseluruhan.
Secara pribadi, tampak lebih jelas bagi saya ketika nilai tes dari bagian persiapan data digunakan nanti dalam pemeriksaan. Atau, Anda dapat membandingkan objek secara eksplisit, misalnya seperti ini:
assertEquals(product, dbProduct);
Dalam pengujian lain untuk memperbarui informasi produk (
updateProduct
), jelas bahwa pembuatan data telah menjadi sedikit lebih rumit dan untuk menjaga integritas visual dari tiga bagian pengujian, mereka dipisahkan oleh dua umpan baris berturut-turut:
@Test public void updateProduct_productUpdated() { Product product = product("productName").build(); productRepository.save(product); Product updatedProduct = product("updatedName").price("1.1").discount("0.5").advertised(true).build(); updatedProduct.setId(product.getId()); productController.updateProduct(product.getId(), updatedProduct); Product dbProduct = productRepository.getOne(product.getId()); assertEquals("updatedName", dbProduct.getName()); assertEquals(number("1.1"), dbProduct.getPrice()); assertEquals(number("0.5"), dbProduct.getDiscount()); assertEquals(true, dbProduct.isAdvertised()); }
Masing-masing dari tiga bagian adonan dapat disederhanakan. Untuk persiapan data, pembangun uji sangat baik, yang berisi logika untuk membuat objek yang nyaman digunakan dari tes. Pemanggilan metode yang terlalu rumit dapat dibuat menjadi metode tambahan di dalam kelas uji, menyembunyikan beberapa parameter yang tidak relevan untuk kelas ini. Untuk menyederhanakan pemeriksaan kompleks, Anda juga dapat menulis fungsi bantu atau mengimplementasikan pencocokan Anda sendiri. Hal utama dengan semua penyederhanaan ini adalah jangan sampai kehilangan visibilitas tes: semuanya harus jelas sekilas pada metode utama, tanpa perlu masuk lebih dalam.
Pembangun ujiPembangun uji patut mendapat perhatian khusus. Enkapsulasi logika membuat objek menyederhanakan perawatan uji. Secara khusus, pengisian bidang model yang tidak relevan dengan pengujian ini dapat disembunyikan di dalam pembangun. Untuk melakukan ini, Anda tidak harus membuatnya secara langsung, tetapi gunakan metode statis yang akan mengisi bidang yang hilang dengan nilai default. Misalnya, jika bidang yang diperlukan baru muncul dalam model, mereka dapat dengan mudah ditambahkan ke metode ini. Di
ProductBuilder
tampilannya seperti ini:
public static ProductBuilder product(String name) { return new ProductBuilder() .name(name) .advertised(false) .price("0.00"); }
Nama ujiSangat penting untuk memahami apa yang secara khusus diuji dalam tes ini. Untuk kejelasan, yang terbaik adalah memberikan jawaban untuk pertanyaan ini dalam judulnya. Menggunakan tes sampel untuk metode
getProduct
pertimbangkan konvensi penamaan yang digunakan:
@Test public void getProduct_oneProductInDb_productReturned() { Product product = product("productName").build(); productRepository.save(product); Product result = productController.getProduct(product.getId()); assertEquals("productName", result.getName()); } @Test public void getProduct_twoProductsInDb_correctProductReturned() { Product product1 = product("product1").build(); Product product2 = product("product2").build(); productRepository.save(product1); productRepository.save(product2); Product result = productController.getProduct(product1.getId()); assertEquals("product1", result.getName()); }
Dalam kasus umum, judul metode pengujian terdiri dari tiga bagian, dipisahkan dengan menggarisbawahi: nama metode yang diuji, skrip, dan hasil yang diharapkan. Namun, tidak ada yang membatalkan akal sehat, dan mungkin dibenarkan untuk menghilangkan beberapa bagian dari nama jika mereka tidak diperlukan dalam konteks ini (misalnya, skrip dalam satu tes untuk membuat produk). Tujuan dari penamaan ini adalah untuk memastikan bahwa esensi dari setiap tes dapat dipahami tanpa mempelajari kode. Ini membuat jendela hasil tes sejelas mungkin, dan dengan itu pekerjaan uji biasanya dimulai.
KesimpulanItu saja. Untuk pertama kalinya, serangkaian minimal empat tes sudah cukup untuk menguji metode kelas
ProductController
. Dalam hal mendeteksi bug, Anda selalu dapat menambahkan tes yang hilang. Pada saat yang sama, jumlah tes minimum secara signifikan mengurangi waktu dan upaya untuk mendukungnya. Ini, pada gilirannya, sangat penting dalam proses implementasi pengujian, karena tes pertama biasanya diperoleh bukan dari kualitas terbaik dan menciptakan banyak masalah yang tidak terduga. Pada saat yang sama, test suite semacam itu cukup untuk menerima bonus yang dijelaskan di bagian pertama artikel.
Perlu dicatat bahwa tes tersebut tidak memeriksa lapisan web aplikasi, tetapi seringkali ini tidak diperlukan. Jika perlu, Anda dapat menulis tes terpisah untuk lapisan web dengan rintisan alih-alih pangkalan (
@WebMvcTest
,
MockMvc
,
@MockBean
) atau menggunakan server yang lengkap. Yang terakhir dapat menyulitkan debugging dan mempersulit pekerjaan dengan transaksi, karena tes tidak dapat mengontrol transaksi server. Contoh dari tes integrasi seperti ini dapat ditemukan di kelas
CustomerControllerServerIT
.
Tes unit
Tes unit memiliki beberapa keunggulan dibandingkan tes integrasi:
- Memulai membutuhkan milidetik;
- Ukuran kecil dari unit yang diuji;
- Sangat mudah untuk mengimplementasikan verifikasi sejumlah besar opsi, karena ketika metode ini dipanggil secara langsung, persiapan data sangat disederhanakan.
Meskipun demikian, tes unit pada dasarnya tidak dapat menjamin pengoperasian aplikasi secara keseluruhan dan tidak memungkinkan Anda untuk menghindari penulisan yang terintegrasi. Jika logika unit yang diuji sederhana, duplikasi tes integrasi dengan tes unit tidak akan membawa manfaat apa pun, tetapi hanya menambah lebih banyak kode untuk mendukung.
Satu-satunya kelas dalam contoh ini yang layak pengujian unit adalah
BonusPointCalculator
. Fitur yang membedakan adalah sejumlah besar cabang logika bisnis. Misalnya, diasumsikan bahwa pembeli menerima bonus 10% dari biaya produk, dikalikan dengan tidak lebih dari 2 pengganda dari daftar berikut:
- Biaya produk lebih dari 10.000 (Γ 4);
- Produk berpartisipasi dalam kampanye iklan (Γ 3);
- Produk adalah produk "favorit" pelanggan (Γ 5);
- Klien memiliki status premium (Γ 2);
- Jika klien memiliki status premium dan membeli produk "favorit", alih-alih dua pengganda yang ditunjukkan, satu (Γ 8) digunakan.
Dalam kehidupan nyata, tentu saja, ada baiknya merancang mekanisme universal yang fleksibel untuk menghitung bonus ini, tetapi untuk menyederhanakan contoh, kita membatasi diri kita pada implementasi yang pasti. Kode perhitungan pengali terlihat seperti ini:
private List<BigDecimal> calculateMultipliers(Customer customer, Product product) { List<BigDecimal> multipliers = new ArrayList<>(); if (customer.getFavProduct() != null && customer.getFavProduct().equals(product)) { if (customer.isPremium()) { multipliers.add(PREMIUM_FAVORITE_MULTIPLIER); } else { multipliers.add(FAVORITE_MULTIPLIER); } } else if (customer.isPremium()) { multipliers.add(PREMIUM_MULTIPLIER); } if (product.isAdvertised()) { multipliers.add(ADVERTISED_MULTIPLIER); } if (product.getPrice().compareTo(EXPENSIVE_THRESHOLD) >= 0) { multipliers.add(EXPENSIVE_MULTIPLIER); } return multipliers; }
Sejumlah besar opsi mengarah pada fakta bahwa dua atau tiga tes integrasi tidak dibatasi di sini. Serangkaian tes unit minimalis sangat cocok untuk men-debug fungsionalitas tersebut.
Suite tes yang sesuai dapat ditemukan di kelas
BonusPointCalculatorTest
. Inilah beberapa di antaranya:
@Test public void calculate_oneProduct() { Product product = product("product").price("1.00").build(); Customer customer = customer("customer").build(); Map<Product, Long> quantities = mapOf(product, 1L); BigDecimal bonus = bonusPointCalculator.calculate(customer, list(product), quantities::get); BigDecimal expectedBonus = bonusPoints("0.10").build(); assertEquals(expectedBonus, bonus); } @Test public void calculate_favProduct() { Product product = product("product").price("1.00").build(); Customer customer = customer("customer").favProduct(product).build(); Map<Product, Long> quantities = mapOf(product, 1L); BigDecimal bonus = bonusPointCalculator.calculate(customer, list(product), quantities::get); BigDecimal expectedBonus = bonusPoints("0.10").addMultiplier(FAVORITE_MULTIPLIER).build(); assertEquals(expectedBonus, bonus); }
Perlu dicatat bahwa dalam pengujian kami merujuk secara khusus ke API publik kelas - metode
calculate
. Menguji kontrak kelas alih-alih implementasinya menghindari kerusakan pengujian karena perubahan dan refactoring yang tidak fungsional.
Akhirnya, ketika kami memeriksa logika internal dengan unit test, kami tidak perlu lagi memasukkan semua detail ini ke dalam integrasi. Dalam hal ini, satu atau lebih tes representatif sudah cukup, misalnya ini:
@Test public void calculateBonusPoints_twoProductTypes_correctValueCalculated() { Product product1 = product("product1").price("1.01").build(); Product product2 = product("product2").price("10.00").build(); productRepository.save(product1); productRepository.save(product2); Customer customer = customer("customer").build(); customerRepository.save(customer); Map<Long, Long> quantities = mapOf(product1.getId(), 1L, product2.getId(), 2L); BigDecimal bonus = customerController.calculateBonusPoints( new CalculateBonusPointsRequest("customer", quantities) ); BigDecimal bonusPointsProduct1 = bonusPoints("0.10").build(); BigDecimal bonusPointsProduct2 = bonusPoints("1.00").quantity(2).build(); BigDecimal expectedBonus = bonusPointsProduct1.add(bonusPointsProduct2); assertEquals(expectedBonus, bonus); }
Seperti dalam kasus tes integrasi, set unit test yang digunakan sangat kecil dan tidak menjamin kebenaran lengkap dari aplikasi. Namun demikian, kehadirannya secara signifikan meningkatkan kepercayaan terhadap kode, memfasilitasi debugging, dan memberikan bonus lain yang tercantum di bagian pertama artikel.
Rekomendasi Implementasi
Saya harap bagian sebelumnya cukup untuk meyakinkan setidaknya satu pengembang untuk mulai menggunakan tes dalam proyek mereka. Bab ini akan secara singkat mencantumkan rekomendasi utama yang akan membantu menghindari masalah serius dan mengarah pada biaya implementasi awal yang lebih rendah.
Cobalah untuk mulai menerapkan tes pada aplikasi baru. Menulis tes pertama dalam proyek lawas yang besar akan jauh lebih sulit dan membutuhkan lebih banyak keterampilan daripada yang baru dibuat. Karena itu, jika memungkinkan lebih baik untuk memulai dengan aplikasi baru yang kecil. Jika aplikasi baru yang lengkap tidak diharapkan, Anda dapat mencoba mengembangkan beberapa utilitas yang bermanfaat untuk penggunaan internal. Hal utama adalah bahwa tugas harus lebih atau kurang realistis - contoh yang ditemukan tidak akan memberikan pengalaman yang lengkap.
Siapkan uji coba reguler. Jika tes tidak berjalan secara teratur, maka mereka tidak hanya berhenti melakukan fungsi utama mereka - memeriksa kebenaran kode - tetapi juga dengan cepat menjadi usang. Oleh karena itu, sangat penting untuk mengkonfigurasi setidaknya pipa CI minimum dengan peluncuran uji otomatis setiap kali kode diperbarui dalam repositori.
Jangan mengejar penutup. Seperti halnya teknologi lainnya, pada awalnya tes tidak akan diperoleh dengan kualitas terbaik. Literatur yang relevan (tautan di akhir artikel) atau mentor yang kompeten dapat membantu di sini, tetapi ini tidak membatalkan kebutuhan untuk kerucut yang diisi sendiri. Pengujian dalam hal ini mirip dengan sisa kode: untuk memahami bagaimana mereka akan mempengaruhi proyek, Anda hanya dapat setelah tinggal bersama mereka untuk sementara waktu. Oleh karena itu, untuk meminimalkan kerusakan, pertama kali lebih baik tidak mengejar jumlah dan jumlah yang indah seperti cakupan seratus persen. Sebaliknya, Anda harus membatasi diri pada skenario positif utama untuk fungsionalitas aplikasi Anda sendiri.
Jangan terbawa oleh unit test. Melanjutkan tema "kuantitas vs kualitas", perlu dicatat bahwa pengujian unit jujur ββtidak boleh dilakukan untuk pertama kalinya, karena ini dapat dengan mudah menyebabkan spesifikasi aplikasi yang berlebihan. Pada gilirannya, ini akan menjadi faktor penghambat yang serius dalam perbaikan aplikasi dan refactoring berikutnya. Tes unit hanya boleh digunakan jika ada logika kompleks di kelas atau kelompok kelas tertentu, yang tidak nyaman untuk diperiksa di tingkat integrasi.
Jangan terbawa oleh kelas rintisan dan metode aplikasi. Rintisan bertopik (stub, mock) adalah alat lain yang membutuhkan pendekatan yang seimbang dan menjaga keseimbangan. Di satu sisi, isolasi lengkap unit memungkinkan Anda untuk fokus pada logika yang diuji dan tidak memikirkan sisa sistem. Di sisi lain, ini akan membutuhkan waktu pengembangan tambahan dan, seperti halnya tes unit, dapat menyebabkan spesifikasi perilaku yang berlebihan.
Lepaskan tes integrasi dari sistem eksternal. Kesalahan yang sangat umum dalam tes integrasi adalah penggunaan database nyata, antrian pesan, dan sistem lain di luar aplikasi. Tentu saja, kemampuan untuk menjalankan tes di lingkungan nyata berguna untuk debugging dan pengembangan. Tes semacam itu dalam jumlah kecil mungkin masuk akal, terutama untuk berlari secara interaktif. Namun, penggunaannya yang meluas menyebabkan sejumlah masalah:
- Untuk menjalankan tes, Anda harus mengkonfigurasi lingkungan eksternal. Misalnya, instal database pada setiap mesin tempat aplikasi akan dirakit. Ini akan mempersulit pengembang baru untuk memasuki proyek dan mengkonfigurasi CI.
- Keadaan sistem eksternal dapat bervariasi pada mesin yang berbeda sebelum menjalankan pengujian. Misalnya, database mungkin sudah berisi tabel yang dibutuhkan aplikasi dengan data yang tidak diharapkan dalam pengujian. Ini akan menyebabkan kegagalan yang tidak terduga dalam tes, dan eliminasi mereka akan membutuhkan waktu yang cukup lama.
- Jika pekerjaan paralel sedang dilakukan pada beberapa proyek, pengaruh yang tidak jelas dari beberapa proyek pada yang lain adalah mungkin. Misalnya, pengaturan basis data khusus yang dibuat untuk salah satu proyek dapat membantu fungsionalitas proyek lain bekerja dengan benar, yang, bagaimanapun, akan pecah ketika diluncurkan pada database yang bersih di mesin lain.
- Tes dilakukan untuk waktu yang lama: putaran penuh dapat mencapai puluhan menit. Ini mengarah pada fakta bahwa pengembang berhenti menjalankan tes secara lokal dan melihat hasilnya hanya setelah mengirim perubahan ke repositori jarak jauh. Perilaku ini meniadakan sebagian besar keuntungan dari tes, yang dibahas pada bagian pertama artikel.
Kosongkan konteks antara tes integrasi. Seringkali, untuk mempercepat kerja tes integrasi, Anda harus menggunakan kembali konteks yang sama di antara mereka. Bahkan dokumentasi resmi Spring membuat rekomendasi semacam itu. Dalam hal ini, pengaruh tes satu sama lain harus dihindari. Karena mereka diluncurkan dalam urutan yang sewenang-wenang, keberadaan koneksi tersebut dapat menyebabkan kesalahan acak yang tidak dapat diproduksi kembali. Untuk mencegah hal ini terjadi, tes tidak boleh meninggalkan perubahan dalam konteks. Misalnya, ketika menggunakan database, untuk isolasi biasanya cukup untuk memutar kembali semua transaksi yang dilakukan dalam pengujian. Jika perubahan pada konteks tidak dapat dihindari, Anda dapat mengonfigurasi rekreasi menggunakan anotasi
@DirtiesContext
.
, . , - . , . , , β , .
. , , . , , .
TDD (Test-Driven Development). TDD , , . , , . , , .
, ?
, :
- ( )? .
- , ( , CI)? .
- ? .
- ? . , , .
, . , , - . β .
Kesimpulan
, . - , . , - . β , , -. , .
, , , !
GitHub