Mengoptimumkan Ujian Integrasi Musim Semi

1. Pengenalan

Dalam artikel ini, kita akan mengadakan perbincangan holistik mengenai ujian integrasi menggunakan Spring dan cara mengoptimumkannya.

Pertama, kita akan membincangkan secara ringkas kepentingan ujian integrasi dan tempatnya dalam Perisian moden yang memfokuskan diri pada ekosistem Spring.

Kemudian, kami akan merangkumi pelbagai senario, dengan memberi tumpuan kepada aplikasi web.

Seterusnya, kita akan membincangkan beberapa strategi untuk meningkatkan kelajuan pengujian , dengan mempelajari tentang pendekatan yang berbeza yang dapat mempengaruhi cara kita membentuk ujian dan cara kita membentuk aplikasi itu sendiri.

Sebelum memulakan, penting untuk diingat bahawa ini adalah artikel pendapat berdasarkan pengalaman. Sebilangan perkara ini mungkin sesuai dengan anda, ada juga yang tidak.

Akhirnya, artikel ini menggunakan Kotlin untuk sampel kod untuk membuatnya sesingkat mungkin, tetapi konsepnya tidak spesifik untuk bahasa ini dan coretan kod harus terasa bermakna bagi pemaju Java dan Kotlin.

2. Ujian Integrasi

Ujian integrasi adalah bahagian asas dari suite ujian automatik. Walaupun tidak semestinya ujian unit jika kita mengikuti piramid ujian yang sihat. Dengan bergantung pada kerangka kerja seperti Spring, kita memerlukan banyak ujian integrasi untuk mengurangkan risiko tingkah laku sistem kita.

Semakin kita mempermudah kod kita dengan menggunakan modul Spring (data, keselamatan, sosial…), semakin besar keperluan untuk ujian integrasi. Ini menjadi kenyataan apabila kita memindahkan banyak infrastruktur kita ke kelas @Configuration .

Kita tidak harus "menguji kerangka kerja", tetapi kita harus memastikan kerangka dikonfigurasi untuk memenuhi kebutuhan kita.

Ujian integrasi membantu kami membina keyakinan tetapi harganya:

  • Itu adalah kelajuan pelaksanaan yang lebih perlahan, yang bermaksud binaan yang lebih perlahan
  • Juga, ujian integrasi menunjukkan skop pengujian yang lebih luas yang tidak ideal dalam kebanyakan kes

Dengan ini, kami akan berusaha mencari beberapa penyelesaian untuk mengurangkan masalah yang disebutkan di atas.

3. Menguji Aplikasi Web

Spring membawa beberapa pilihan untuk menguji aplikasi web, dan kebanyakan pembangun Spring sudah biasa dengannya, ini adalah:

  • MockMvc : Mengolok-olokkan servlet API, berguna untuk aplikasi web yang tidak reaktif
  • TestRestTemplate : Boleh digunakan menunjuk ke aplikasi kami, berguna untuk aplikasi web yang tidak reaktif di mana servlet ejekan tidak diinginkan
  • WebTestClient: Merupakan alat pengujian untuk aplikasi web reaktif, baik dengan permintaan / respons yang diejek atau memukul pelayan yang sebenarnya

Oleh kerana kita sudah mempunyai artikel yang merangkumi topik ini, kita tidak akan menghabiskan masa untuk membicarakannya.

Jangan ragu untuk melihat jika anda ingin menggali lebih mendalam.

4. Mengoptimumkan Masa Pelaksanaan

Ujian integrasi sangat bagus. Mereka memberi kita keyakinan yang tinggi. Juga jika dilaksanakan dengan tepat, mereka dapat menggambarkan maksud aplikasi kita dengan cara yang sangat jelas, dengan suara mengejek dan penyiapan yang lebih sedikit.

Namun, apabila aplikasi kita semakin matang dan perkembangannya semakin meningkat, masa membina pasti akan meningkat. Seiring bertambahnya waktu, mungkin tidak praktikal untuk terus menjalankan semua ujian setiap masa.

Selepas itu, memberi kesan kepada maklum balas kami dan meneruskan kaedah pembangunan terbaik.

Tambahan pula, ujian integrasi sememangnya mahal. Memulakan kegigihan, menghantar permintaan melalui (walaupun mereka tidak pernah meninggalkan localhost ), atau melakukan beberapa IO hanya memerlukan masa.

Sangat penting untuk mengawasi masa pembuatan kami, termasuk pelaksanaan ujian. Dan ada beberapa muslihat yang dapat kita gunakan pada musim bunga untuk memastikannya rendah.

Pada bahagian seterusnya, kami akan membahas beberapa perkara untuk membantu kami mengoptimumkan masa pembinaan kami serta beberapa perangkap yang mungkin mempengaruhi kelajuannya:

  • Menggunakan profil dengan bijak - bagaimana profil mempengaruhi prestasi
  • Menimbangkan semula @MockBean - bagaimana mengejek memukul prestasi
  • Refactoring @MockBean - alternatif untuk meningkatkan prestasi
  • Berfikir dengan teliti mengenai @ DirtiesContext - anotasi yang berguna tetapi berbahaya dan bagaimana tidak menggunakannya
  • Menggunakan slice ujian - alat hebat yang dapat membantu atau meneruskan perjalanan
  • Menggunakan pewarisan kelas - cara untuk mengatur ujian dengan cara yang selamat
  • Pengurusan negeri - amalan yang baik untuk mengelakkan ujian tidak stabil
  • Memfokuskan semula ke dalam ujian unit - kaedah terbaik untuk mendapatkan binaan yang kukuh dan selesa

Mari kita mulakan!

4.1. Menggunakan Profil dengan Bijak

Profil adalah alat yang cukup kemas. Yaitu, tag mudah yang dapat mengaktifkan atau mematikan kawasan tertentu dari Aplikasi kami. Kita juga boleh menggunakan bendera ciri dengan mereka!

Oleh kerana profil kami semakin kaya, sangat menggoda untuk menukar setiap saat dalam ujian integrasi kami. Terdapat alat mudah untuk melakukannya, seperti @ActiveProfiles . Namun, setiap kali kita melakukan ujian dengan profil baru, ApplicationContext baru akan dibuat.

Membuat konteks aplikasi mungkin selesa dengan aplikasi boot vanilla spring tanpa apa-apa di dalamnya. Tambahkan ORM dan beberapa modul dan ia akan cepat melonjak hingga 7+ saat.

Tambahkan sekumpulan profil, dan sebarkannya melalui beberapa ujian dan kami akan mendapat binaan 60+ saat dengan cepat (dengan andaian kami menjalankan ujian sebagai sebahagian daripada binaan kami - dan semestinya).

Sebaik sahaja kita menghadapi aplikasi yang cukup rumit, memperbaikinya sangat menakutkan. Walau bagaimanapun, jika kita merancang dengan teliti terlebih dahulu, menjadi remeh untuk menyimpan masa membina yang masuk akal.

Terdapat beberapa muslihat yang perlu kita ingat mengenai profil dalam ujian integrasi:

  • Buat profil agregat, iaitu ujian , sertakan semua profil yang diperlukan di dalamnya - berpegang teguh pada profil ujian kami di mana sahaja
  • Reka profil kami dengan mempertimbangkan kebolehpercayaan. Sekiranya kita terpaksa menukar profil mungkin ada cara yang lebih baik
  • Nyatakan profil ujian kami di tempat terpusat - kami akan membincangkannya kemudian
  • Elakkan menguji semua kombinasi profil. Sebagai alternatif, kita boleh mempunyai set ujian e2e per persekitaran yang menguji aplikasi dengan set profil khusus itu

4.2. Masalah dengan @MockBean

@MockBean adalah alat yang cukup hebat.

Apabila kita memerlukan sihir Spring tetapi ingin mengejek komponen tertentu, @MockBean sangat berguna. Tetapi ia berlaku pada harga.

Setiap kali @MockBean muncul di kelas, cache ApplicationContext ditandai sebagai kotor, oleh itu pelari akan membersihkan cache setelah kelas ujian selesai. Yang sekali lagi menambah sekumpulan detik tambahan untuk membina kami.

Ini adalah kontroversi, tetapi cuba menggunakan aplikasi sebenar dan bukannya mengejek kerana senario ini dapat membantu. Sudah tentu, tidak ada peluru perak di sini. Batas menjadi kabur apabila kita tidak membiarkan diri kita mengejek pergantungan.

Kita mungkin berfikir: Mengapa kita bertahan ketika semua yang ingin kita uji adalah lapisan REST kita? Ini adalah titik yang adil, dan selalu ada kompromi.

Namun, dengan mempertimbangkan beberapa prinsip, ini sebenarnya dapat berubah menjadi kelebihan yang menghasilkan reka bentuk yang lebih baik untuk kedua-dua ujian dan aplikasi kami dan mengurangkan masa ujian.

4.3. refactoring @MockBean

Pada bahagian ini, kami akan cuba menguji semula ujian 'lambat' menggunakan @MockBean untuk menjadikannya menggunakan semula ApplicationContext yang di- cache .

Anggaplah kita mahu menguji POST yang mencipta pengguna. Sekiranya kita mengejek - menggunakan @MockBean , kita hanya dapat mengesahkan bahawa perkhidmatan kami telah dipanggil dengan pengguna bersiri yang baik.

Sekiranya kami menguji perkhidmatan kami dengan betul, pendekatan ini seharusnya mencukupi:

class UsersControllerIntegrationTest : AbstractSpringIntegrationTest() { @Autowired lateinit var mvc: MockMvc @MockBean lateinit var userService: UserService @Test fun links() { mvc.perform(post("/users") .contentType(MediaType.APPLICATION_JSON) .content("""{ "name":"jose" }""")) .andExpect(status().isCreated) verify(userService).save("jose") } } interface UserService { fun save(name: String) }

Kami mahu mengelakkan @MockBean . Oleh itu, kita akan terus bertahan dengan entiti (dengan andaian itulah yang dilakukan oleh perkhidmatan).

Pendekatan yang paling naif di sini adalah untuk menguji kesan sampingan: Selepas POSTING, pengguna saya berada di DB saya, dalam contoh kami, ini akan menggunakan JDBC.

Namun, ini melanggar batasan ujian:

@Test fun links() { mvc.perform(post("/users") .contentType(MediaType.APPLICATION_JSON) .content("""{ "name":"jose" }""")) .andExpect(status().isCreated) assertThat( JdbcTestUtils.countRowsInTable(jdbcTemplate, "users")) .isOne() }

Dalam contoh khusus ini, kami melanggar batasan pengujian kerana kami memperlakukan aplikasi kami sebagai kotak hitam HTTP untuk mengirim pengguna, tetapi kemudian kami menegaskan menggunakan perincian pelaksanaan, iaitu, pengguna kami telah bertahan di beberapa DB.

Sekiranya kita menggunakan aplikasi kita melalui HTTP, bisakah kita menegaskan hasilnya melalui HTTP juga?

@Test fun links() { mvc.perform(post("/users") .contentType(MediaType.APPLICATION_JSON) .content("""{ "name":"jose" }""")) .andExpect(status().isCreated) mvc.perform(get("/users/jose")) .andExpect(status().isOk) }

Terdapat beberapa kelebihan jika kita mengikuti pendekatan terakhir:

  • Ujian kami akan bermula lebih cepat (boleh dikatakan, mungkin memerlukan sedikit masa lagi untuk dilaksanakan, tetapi ia harus dibayar kembali)
  • Juga, ujian kami tidak menyedari kesan sampingan yang tidak berkaitan dengan had HTTP iaitu DB
  • Akhirnya, ujian kami menyatakan dengan jelas maksud sistem: Sekiranya anda POST, anda dapat MENDAPATKAN Pengguna

Sudah tentu, ini tidak mungkin berlaku kerana pelbagai sebab:

  • Kami mungkin tidak mempunyai titik akhir 'kesan sampingan': Pilihan di sini adalah mempertimbangkan untuk membuat 'titik akhir pengujian'
  • Kerumitan terlalu tinggi untuk mencapai keseluruhan aplikasi: Pilihan di sini adalah untuk mempertimbangkan potongan (kita akan membincangkannya kemudian)

4.4. Berfikir dengan Cermat Mengenai @DirtiesContext

Kadang kala, kita mungkin perlu mengubahsuai ApplicationContext dalam ujian kita. Untuk senario ini, @DirtiesContext memberikan fungsi yang tepat.

Atas sebab yang sama yang dinyatakan di atas, @DirtiesContext adalah sumber yang sangat mahal ketika datang ke masa pelaksanaan, dan dengan itu, kita harus berhati-hati.

Beberapa penyalahgunaan @DirtiesContext termasuk tetapan semula cache aplikasi atau dalam set semula memori DB. Terdapat cara yang lebih baik untuk menangani senario ini dalam ujian integrasi, dan kami akan membahas beberapa bahagian selanjutnya.

4.5. Menggunakan Slice Ujian

Test Slices are a Spring Boot feature introduced in the 1.4. The idea is fairly simple, Spring will create a reduced application context for a specific slice of your app.

Also, the framework will take care of configuring the very minimum.

There are a sensible number of slices available out of the box in Spring Boot and we can create our own too:

  • @JsonTest: Registers JSON relevant components
  • @DataJpaTest: Registers JPA beans, including the ORM available
  • @JdbcTest: Useful for raw JDBC tests, takes care of the data source and in memory DBs without ORM frills
  • @DataMongoTest: Tries to provide an in-memory mongo testing setup
  • @WebMvcTest: A mock MVC testing slice without the rest of the app
  • … (we can check the source to find them all)

This particular feature if used wisely can help us build narrow tests without such a big penalty in terms of performance particularly for small/medium sized apps.

However, if our application keeps growing it also piles up as it creates one (small) application context per slice.

4.6. Using Class Inheritance

Using a single AbstractSpringIntegrationTest class as the parent of all our integration tests is a simple, powerful and pragmatic way of keeping the build fast.

If we provide a solid setup, our team will simply extend it, knowing that everything ‘just works'. This way we can worry less about managing state or configuring the framework and focus on the problem at hand.

We could set all the test requirements there:

  • The Spring runner – or preferably rules, in case we need other runners later
  • profiles – ideally our aggregate test profile
  • initial config – setting the state of our application

Let's have a look at a simple base class that takes care of the previous points:

@SpringBootTest @ActiveProfiles("test") abstract class AbstractSpringIntegrationTest { @Rule @JvmField val springMethodRule = SpringMethodRule() companion object { @ClassRule @JvmField val SPRING_CLASS_RULE = SpringClassRule() } }

4.7. State Management

It's important to remember where ‘unit' in Unit Test comes from. Simply put, it means we can run a single test (or a subset) at any point getting consistent results.

Hence, the state should be clean and known before every test starts.

In other words, the result of a test should be consistent regardless of whether it is executed in isolation or together with other tests.

This idea applies just the same to integration tests. We need to ensure our app has a known (and repeatable) state before starting a new test. The more components we reuse to speed things up (app context, DBs, queues, files…), the more chances to get state pollution.

Assuming we went all in with class inheritance, now, we have a central place to manage state.

Let's enhance our abstract class to make sure our app is in a known state before running tests.

In our example, we'll assume there are several repositories (from various data sources), and a Wiremock server:

@SpringBootTest @ActiveProfiles("test") @AutoConfigureWireMock(port = 8666) @AutoConfigureMockMvc abstract class AbstractSpringIntegrationTest { //... spring rules are configured here, skipped for clarity @Autowired protected lateinit var wireMockServer: WireMockServer @Autowired lateinit var jdbcTemplate: JdbcTemplate @Autowired lateinit var repos: Set
    
      @Autowired lateinit var cacheManager: CacheManager @Before fun resetState() { cleanAllDatabases() cleanAllCaches() resetWiremockStatus() } fun cleanAllDatabases() { JdbcTestUtils.deleteFromTables(jdbcTemplate, "table1", "table2") jdbcTemplate.update("ALTER TABLE table1 ALTER COLUMN id RESTART WITH 1") repos.forEach { it.deleteAll() } } fun cleanAllCaches() { cacheManager.cacheNames .map { cacheManager.getCache(it) } .filterNotNull() .forEach { it.clear() } } fun resetWiremockStatus() { wireMockServer.resetAll() // set default requests if any } }
    

4.8. Refactoring into Unit Tests

This is probably one of the most important points. We'll find ourselves over and over with some integration tests that are actually exercising some high-level policy of our app.

Whenever we find some integration tests testing a bunch of cases of core business logic, it's time to rethink our approach and break them down into unit tests.

A possible pattern here to accomplish this successfully could be:

  • Identify integration tests that are testing multiple scenarios of core business logic
  • Duplicate the suite, and refactor the copy into unit Tests – at this stage, we might need to break down the production code too to make it testable
  • Get all tests green
  • Leave a happy path sample that is remarkable enough in the integration suite – we might need to refactor or join and reshape a few
  • Remove the remaining integration Tests

Michael Feathers covers many techniques to achieve this and more in Working Effectively with Legacy Code.

5. Summary

In this article, we had an introduction to Integration tests with a focus on Spring.

Pertama, kami membincangkan kepentingan ujian integrasi dan mengapa ia sangat relevan dalam aplikasi Spring.

Selepas itu, kami merangkum beberapa alat yang mungkin berguna untuk jenis ujian Integrasi tertentu dalam Aplikasi Web.

Akhirnya, kami menjalani senarai kemungkinan masalah yang memperlahankan masa pelaksanaan ujian kami, serta trik untuk memperbaikinya.