Refleksi Pengujian Perusahaan yang Efektif

Halo, Habr!

Baru-baru ini, kami telah kembali ke studi menyeluruh tentang topik pengujian, dan dalam rencana yang akan datang kami bahkan memiliki buku yang sangat bagus tentang Unit Testing. Pada saat yang sama, kami percaya bahwa konteks penting dalam topik ini karena tidak ada tempat lain, oleh karena itu hari ini kami menawarkan terjemahan dua publikasi (digabung menjadi satu) yang diterbitkan di blog spesialis Java EE terkemuka Sebastian Dashner - yaitu, 1/6 dan 2/6 dari seri “ Pikiran tentang pengujian perusahaan yang efisien. "

Pengujian perusahaan adalah topik yang belum diperiksa sedetail yang kami inginkan. Dibutuhkan banyak waktu dan upaya untuk menulis dan terutama untuk mendukung tes, namun, mencoba menghemat waktu dengan meninggalkan tes bukanlah pilihan. Berapa volume tugas, pendekatan, dan teknologi pengujian yang perlu ditelusuri untuk meningkatkan efektivitas pengujian?

pengantar


Terlepas dari berbagai jenis tes dan ruang lingkupnya, tujuan mempersiapkan serangkaian tes adalah untuk memastikan pada bahan ini bahwa dalam produksi aplikasi kita akan bekerja persis seperti yang diharapkan. Motivasi seperti itu harus menjadi yang utama ketika memeriksa apakah sistem memenuhi tugas, jika kita mempertimbangkan sistem ini dari sudut pandang pengguna.

Karena rentang perhatian dan pengalihan konteks adalah hal yang harus diperhitungkan, kita harus memastikan bahwa pengujian kami tidak berjalan dan diuji dalam waktu yang lama dan bahwa hasil pengujian dapat diprediksi. Saat menulis kode, verifikasi kode yang cepat (layak dalam satu detik) sangat penting - ini memastikan produktivitas tinggi dan fokus selama bekerja.
Di sisi lain, kita harus memastikan dukungan pengujian. Perubahan perangkat lunak sangat sering, dan dengan cakupan kode yang substansial dengan uji fungsional, setiap perubahan fungsional pada kode produksi akan memerlukan perubahan pada tingkat pengujian. Idealnya, kode pengujian harus berubah hanya ketika fungsionalitas, mis., Logika bisnis, berubah, dan tidak ketika membersihkan kode yang tidak perlu dan refactoring. Secara umum, skenario pengujian harus mencakup kemungkinan perubahan struktural yang tidak fungsional.

Ketika kami mempertimbangkan berbagai bidang penerapan tes, muncul pertanyaan: mana dari bidang ini yang sepadan dengan waktu dan upaya? Misalnya, dalam aplikasi layanan mikro, serta dalam sistem apa pun yang menyediakan pekerjaan signifikan pada distribusi dan integrasi kode, tes integrasi sangat penting, membantu untuk meraba-raba batas-batas sistem. Oleh karena itu, kita memerlukan cara yang efektif untuk menguji seluruh aplikasi secara keseluruhan selama pengembangan lokal, sambil menjaga lingkungan dan struktur aplikasi ini dalam bentuk yang sedekat mungkin dengan produksi.

Prinsip dan Keterbatasan


Terlepas dari solusi yang akan dipilih, mari kita mendefinisikan prinsip dan batasan berikut untuk rangkaian uji kami:

  • , . , . , , .
  • , , . , , , . , , .
  • - . , , .
  • , , . : « HTTP- gRPC, JSON - enterprise-, ..?”.
  • , , -. API, DSL .
  • « », , , , , , “dev” debug () , dev Quarkus', Telepresence, watch-and-deploy (« ») .
  • . , , , , . .
  • , , -, , , . , , , .


Tes unit memeriksa perilaku modul tunggal, biasanya kelas, sementara semua faktor eksternal yang tidak terkait dengan struktur modul diabaikan atau disimulasikan. Tes unit harus memverifikasi logika bisnis masing-masing modul, tanpa memeriksa integrasi atau konfigurasinya lebih lanjut.

Dalam pengalaman saya, sebagian besar pengembang perusahaan memiliki ide yang cukup bagus tentang bagaimana tes unit dikompilasi. Untuk membuat kesan tentang ini, Anda dapat melihat contoh ini di proyek pengujian kopi saya.. Dalam sebagian besar proyek, JUnit digunakan bersama Mockito untuk mensimulasikan dependensi, dan idealnya dengan AssertJ untuk mendefinisikan pernyataan yang dapat dibaca secara efisien. Saya selalu menekankan bahwa unit test dapat dilakukan tanpa ekstensi atau permulaan khusus, yaitu untuk melakukan ini dengan JUnit yang biasa. Penjelasannya sederhana: ini semua tentang runtime, karena kita memerlukan kemampuan untuk menjalankan ratusan tes dalam hitungan milidetik.

Sebagai aturan, unit test berjalan sangat cepat, dan mudah untuk mengumpulkan suite tes yang kompleks atau alur kerja khusus dari mereka, karena mereka mudah dijalankan dan tidak memaksakan batasan pada siklus hidup tes.

Namun, ketika Anda memiliki banyak unit test yang mensimulasikan dependensi dari kelas yang diuji, ada satu kelemahan: mereka terkait erat dengan implementasi (ini terutama berlaku untuk kelas dan metode), itulah sebabnya kode kami sulit untuk diperbaiki. Dengan kata lain, untuk setiap tindakan refactoring dalam kode produksi, itu juga memerlukan perubahan pada kode pengujian. Dalam kasus terburuk, pengembang bahkan mulai menolak sebagian refactoring, hanya karena terlalu memberatkan, dan kualitas kode dalam proyek menurun dengan cepat. Idealnya, pengembang harus dapat memperbaiki dan mengatur ulang elemen-elemen, asalkan karena ini tidak ada perubahan dalam aplikasi yang terlihat oleh pengguna. Tes unit tidak selalu menyederhanakan kode produksi refactoring.

Tetapi pengalaman menunjukkan bahwa tes unit sangat efektif dalam memeriksa kode yang padat diisi dengan logika singkat atau menggambarkan implementasi fungsi tertentu, misalnya, suatu algoritma, dan, pada saat yang sama, tidak berinteraksi sangat aktif dengan komponen lain. Semakin kurang kompleks atau padatnya kode dalam kelas tertentu, semakin sedikit kompleksitas siklomatiknya, atau semakin aktif berinteraksi dengan komponen lain, maka tes unit akan kurang efektif ketika menguji kelas ini. Terutama dalam kasus dengan layanan microser, di mana logika bisnis relatif sedikit terkandung, tetapi integrasi yang luas dengan sistem eksternal disediakan, mungkin ada sedikit kebutuhan untuk menggunakan tes unit dalam banyak. Dalam sistem seperti itu, modul individual (dengan pengecualian langka) biasanya mengandung sedikit logika khusus. Ini harus dipertimbangkan ketika memutuskanapa yang lebih tepat untuk menghabiskan waktu dan usaha.

Menguji situasi aplikasi


Untuk mengatasi masalah yang menghubungkan tes dengan implementasi, Anda dapat mencoba pendekatan yang sedikit berbeda untuk memperluas cakupan tes. Dalam buku saya, saya menulis tentang tes komponen, karena saya tidak dapat menemukan istilah yang lebih baik; tetapi, pada dasarnya, dalam hal ini kita berbicara tentang pengujian situasi yang diterapkan.

Tes situasi aplikasi adalah tes integrasi yang beroperasi pada level kode, yang tidak menggunakan wadah bawaan - mereka ditinggalkan demi mempercepat peluncuran. Mereka menguji logika bisnis komponen yang terkoordinasi dengan baik, yang biasanya digunakan dalam kasus praktis tertentu, dari metode batas - dan kemudian turun ke semua komponen yang terkait dengannya. Integrasi dengan sistem eksternal, misalnya, dengan basis data, ditiru menggunakan tiruan.

Membangun skenario seperti itu tanpa menggunakan teknologi yang lebih maju yang secara otomatis akan menghubungkan komponen tampak seperti pekerjaan besar. Namun, kami mendefinisikan komponen uji yang dapat digunakan kembali, mereka juga merupakan mitra pengujian yang memperluas komponen dengan mensimulasikan, menghubungkan, dan juga menambahkan konfigurasi pengujian; semua ini dilakukan untuk meminimalkan jumlah upaya yang diperlukan untuk refactoring. Tujuannya adalah untuk menciptakan satu-satunya tanggung jawab yang membatasi tingkat pengaruh perubahan pada satu kelas (atau beberapa kelas) di bidang pengujian. Melakukan pekerjaan tersebut dengan tujuan untuk digunakan kembali, kami mengurangi jumlah total pekerjaan yang diperlukan, dan strategi seperti itu dibenarkan ketika proyek tumbuh, tetapi setiap komponen hanya membutuhkan perbaikan kecil, dan pekerjaan ini dengan cepat diamortisasi.

Untuk lebih membayangkan semua ini, anggaplah kita sedang menguji kelas yang menjelaskan urutan kopi. Kelas ini mencakup dua kelas lainnya: CoffeeShopdan OrderProcessor.



Kelas tes berlipat ganda, CoffeeShopTestDoubledan OrderProcessorTestDoublemereka *TDberada di area uji proyek, di mana mereka mewarisi komponen CoffeeShopdan OrderProcessorterletak di area utama program. Rekan uji dapat mengatur simulasi dan logika koneksi yang diperlukan dan berpotensi memperluas antarmuka publik kelas menggunakan metode simulasi yang diperlukan dalam aplikasi ini, atau metode verifikasi.

Berikut ini menunjukkan kelas uji ganda untuk komponen CoffeeShop:

public class CoffeeShopTestDouble extends CoffeeShop {

    public CoffeeShopTestDouble(OrderProcessorTestDouble orderProcessorTestDouble) {
        entityManager = mock(EntityManager.class);
        orderProcessor = orderProcessorTestDouble;
    }

    public void verifyCreateOrder(Order order) {
        verify(entityManager).merge(order);
    }

    public void verifyProcessUnfinishedOrders() {
        verify(entityManager).createNamedQuery(Order.FIND_UNFINISHED, Order.class);
    }

    public void answerForUnfinishedOrders(List<Order> orders) {
        //     
    }
}

Kelas uji ganda dapat mengakses bidang dan konstruktor dari kelas dasar CoffeeShop untuk membangun dependensi. Di sini, dalam bentuk kembar uji, varian komponen lain juga digunakan, khususnya, OrderProcessorTestDoublemereka diperlukan untuk memanggil simulasi tambahan atau metode verifikasi, yang merupakan bagian dari kasus praktis.

Kelas uji ganda adalah komponen yang dapat digunakan kembali, yang masing-masing ditulis satu kali per ruang lingkup setiap proyek, dan kemudian digunakan dalam banyak kasus praktis:

class CoffeeShopTest {

    private CoffeeShopTestDouble coffeeShop;
    private OrderProcessorTestDouble orderProcessor;

    @BeforeEach
    void setUp() {
        orderProcessor = new OrderProcessorTestDouble();
        coffeeShop = new CoffeeShopTestDouble(orderProcessor);
    }

    @Test
    void testCreateOrder() {
        Order order = new Order();
        coffeeShop.createOrder(order);
        coffeeShop.verifyCreateOrder(order);
    }

    @Test
    void testProcessUnfinishedOrders() {
        List<Order> orders = Arrays.asList(...);
        coffeeShop.answerForUnfinishedOrders(orders);

        coffeeShop.processUnfinishedOrders();

        coffeeShop.verifyProcessUnfinishedOrders();
        orderProcessor.verifyProcessOrders(orders);
    }

}

Uji komponen memverifikasi kasus spesifik dari logika bisnis yang dipanggil pada titik masuk, dalam kasus ini CoffeeShop. Tes tersebut diperoleh secara ringkas dan mudah dibaca, karena semua koneksi dan simulasi dilakukan pada kembar uji yang terpisah, dan kemudian mereka dapat menggunakan teknik penyaringan yang sangat khusus, seperti verifyProcessOrders().

Seperti yang Anda lihat, kelas uji memperluas cakupan kelas produksi, memungkinkan Anda untuk menginstal mokee dan menggunakan metode yang memverifikasi perilaku. Terlepas dari kenyataan bahwa tampaknya membuat sistem seperti itu membutuhkan banyak upaya, biaya ini dengan cepat diamortisasi jika, dalam kerangka kerja seluruh proyek, kami memiliki banyak kasus praktis di mana komponen dapat digunakan kembali. Semakin banyak proyek kami tumbuh, semakin berguna pendekatan ini menjadi - terutama jika Anda memperhatikan waktu yang dibutuhkan untuk menyelesaikan tes. Semua test case kami masih berjalan menggunakan JUnit, dan dalam waktu sesingkat mungkin, semuanya dieksekusi dalam ratusan.

Ini adalah manfaat utama dari pendekatan ini: tes komponen berjalan secepat tes unit biasa, namun, mereka merangsang refactoring kode produksi, karena perubahan perlu dilakukan pada satu komponen atau hanya beberapa komponen. Selain itu, meningkatkan rekan uji dengan penyetelan ekspresif dan metode pengujian khusus untuk area subjek kami, kami meningkatkan keterbacaan kode kami, memfasilitasi penggunaannya, dan menyingkirkan kode stereotip dalam skrip uji.

All Articles