Tutorial JUnit 5, bahagian 2: Unit pengujian Spring MVC dengan JUnit 5

Spring MVC adalah salah satu kerangka kerja Java yang paling popular untuk membangun aplikasi Java perusahaan, dan sangat sesuai untuk pengujian. Dengan reka bentuk, Spring MVC mempromosikan pemisahan kebimbangan dan mendorong pengekodan terhadap antara muka. Kualiti ini, bersama dengan pelaksanaan suntikan ketergantungan Spring, menjadikan aplikasi Spring sangat mudah diuji.

Tutorial ini adalah separuh kedua pengenalan saya untuk pengujian unit dengan JUnit 5. Saya akan menunjukkan kepada anda cara mengintegrasikan JUnit 5 dengan Spring, kemudian memperkenalkan anda kepada tiga alat yang boleh anda gunakan untuk menguji pengawal, perkhidmatan dan repositori Spring MVC.

muat turun Dapatkan kod Muat turun kod sumber misalnya aplikasi yang digunakan dalam tutorial ini. Dicipta oleh Steven Haines untuk JavaWorld.

Mengintegrasikan JUnit 5 dengan Spring 5

Untuk tutorial ini, kami menggunakan Maven dan Spring Boot, jadi perkara pertama yang perlu kami lakukan ialah menambahkan kebergantungan JUnit 5 ke fail Maven POM kami:

  org.junit.jupiter junit-jupiter 5.6.0 test  

Sama seperti yang kami lakukan di Bahagian 1, kami akan menggunakan Mockito untuk contoh ini. Oleh itu, kita perlu menambah perpustakaan JUnit 5 Mockito:

  org.mockito mockito-junit-jupiter 3.2.4 test  

@ExtendWith dan kelas SpringExtension

JUnit 5 mendefinisikan antara muka peluasan , di mana kelas dapat disatukan dengan ujian JUnit pada pelbagai peringkat kitaran hayat pelaksanaan. Kami boleh mengaktifkan peluasan dengan menambahkan @ExtendWithanotasi ke kelas ujian kami dan menentukan kelas peluasan yang akan dimuat. Sambungan tersebut kemudian dapat melaksanakan pelbagai antara muka panggilan balik, yang akan dipanggil sepanjang kitaran hayat ujian: sebelum semua ujian dijalankan, sebelum setiap ujian dijalankan, setelah setiap ujian dijalankan, dan setelah semua ujian dijalankan.

Spring menentukan SpringExtensionkelas yang melanggan pemberitahuan kitaran hidup JUnit 5 untuk membuat dan mengekalkan "konteks ujian." Ingat bahawa konteks aplikasi Spring mengandungi semua kacang Spring dalam aplikasi dan bahawa ia melaksanakan suntikan kebergantungan untuk menyatukan aplikasi dan kebergantungannya. Spring menggunakan model pelanjutan JUnit 5 untuk mengekalkan konteks aplikasi ujian, yang menjadikan ujian unit tulisan dengan Spring mudah.

Setelah kami menambahkan perpustakaan JUnit 5 ke fail Maven POM kami, kami dapat menggunakan SpringExtension.classuntuk memperluas kelas ujian JUnit 5 kami:

 @ExtendWith(SpringExtension.class) class MyTests { // ... }

Contohnya, dalam kes ini, adalah aplikasi Spring Boot. Nasib baik @SpringBootTestanotasi sudah termasuk @ExtendWith(SpringExtension.class)anotasi, jadi kami hanya perlu memasukkan @SpringBootTest.

Menambah kebergantungan Mockito

Untuk menguji setiap komponen secara terpencil dan mensimulasikan senario yang berbeza, kami ingin membuat pelaksanaan tiruan dari setiap kelas. Di sinilah Mockito masuk. Sertakan kebergantungan berikut dalam fail POM anda untuk menambahkan sokongan untuk Mockito:

  org.mockito mockito-junit-jupiter 3.2.4 test  

Setelah anda menyatukan JUnit 5 dan Mockito ke dalam aplikasi Spring anda, anda boleh memanfaatkan Mockito dengan hanya menentukan Spring bean (seperti perkhidmatan atau repositori) di kelas ujian anda menggunakan @MockBeananotasi. Inilah contoh kami:

 @SpringBootTest public class WidgetServiceTest { /** * Autowire in the service we want to test */ @Autowired private WidgetService service; /** * Create a mock implementation of the WidgetRepository */ @MockBean private WidgetRepository repository; ... } 

Dalam contoh ini, kami membuat ejekan WidgetRepositorydi dalam WidgetServiceTestkelas kami . Apabila Spring melihatnya, ia akan memasukkannya secara automatik ke dalam kami WidgetServicesehingga kami dapat membuat senario yang berbeza dalam kaedah ujian kami. Setiap kaedah ujian akan mengkonfigurasi tingkah laku WidgetRepository, seperti dengan mengembalikan permintaan yang diminta Widgetatau mengembalikan Optional.empty()pertanyaan untuk data yang tidak dijumpai. Kami akan menghabiskan sisa tutorial ini untuk melihat contoh pelbagai cara untuk mengkonfigurasi kacang tiruan ini.

Aplikasi contoh Spring MVC

Untuk menulis ujian unit berasaskan musim bunga, kami memerlukan aplikasi untuk menulisnya. Nasib baik, kita dapat menggunakan contoh aplikasi dari tutorial Spring Series saya "Menguasai Spring Spring 5, Bahagian 1: Spring MVC." Saya menggunakan aplikasi contoh dari tutorial itu sebagai aplikasi asas. Saya mengubahnya dengan REST API yang lebih kuat sehingga kami mempunyai beberapa perkara lagi untuk diuji.

Aplikasi contohnya adalah aplikasi web Spring MVC dengan pengawal REST, lapisan perkhidmatan, dan repositori yang menggunakan JPA Spring Data untuk meneruskan "widget" ke dan dari pangkalan data dalam memori H2. Rajah 1 adalah gambaran keseluruhan.

Steven Haines

Apa itu widget?

A Widgethanyalah "benda" dengan ID, nama, keterangan, dan nombor versi. Dalam kes ini, widget kami diberi penjelasan dengan anotasi JPA untuk menentukannya sebagai entiti. Ini WidgetRestControlleradalah pengawal Spring MVC yang menterjemahkan panggilan RESTful API menjadi tindakan untuk dilaksanakan Widgets. Ini WidgetServiceadalah perkhidmatan Spring standard yang menentukan fungsi perniagaan untuk Widgets. Akhirnya, WidgetRepositoryadalah antara muka Spring Data JPA, yang mana Spring akan membuat pelaksanaan pada waktu runtime. Kami akan menyemak kod untuk setiap kelas semasa kami menulis ujian di bahagian seterusnya.

Unit menguji perkhidmatan Spring

Mari kita mulakan dengan mengkaji bagaimana untuk menguji Spring  perkhidmatan , kerana itu adalah komponen yang paling mudah dalam permohonan MVC kami untuk menguji. Contoh dalam bahagian ini akan membolehkan kita meneroka integrasi JUnit 5 dengan Spring tanpa memperkenalkan komponen ujian atau perpustakaan baru, walaupun kita akan melakukannya kemudian dalam tutorial.

Kami akan memulakan dengan mengkaji WidgetServiceantara muka dan WidgetServiceImplkelas, yang masing-masing ditunjukkan dalam Penyenaraian 1 dan Penyenaraian 2.

Penyenaraian 1. Antara muka perkhidmatan Spring (WidgetService.java)

 package com.geekcap.javaworld.spring5mvcexample.service; import com.geekcap.javaworld.spring5mvcexample.model.Widget; import java.util.List; import java.util.Optional; public interface WidgetService { Optional findById(Long id); List findAll(); Widget save(Widget widget); void deleteById(Long id); }

Penyenaraian 2. Kelas pelaksanaan perkhidmatan Spring (WidgetServiceImpl.java)

 package com.geekcap.javaworld.spring5mvcexample.service; import com.geekcap.javaworld.spring5mvcexample.model.Widget; import com.geekcap.javaworld.spring5mvcexample.repository.WidgetRepository; import com.google.common.collect.Lists; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.Optional; @Service public class WidgetServiceImpl implements WidgetService { private WidgetRepository repository; public WidgetServiceImpl(WidgetRepository repository) { this.repository = repository; } @Override public Optional findById(Long id) { return repository.findById(id); } @Override public List findAll() { return Lists.newArrayList(repository.findAll()); } @Override public Widget save(Widget widget) { // Increment the version number widget.setVersion(widget.getVersion()+1); // Save the widget to the repository return repository.save(widget); } @Override public void deleteById(Long id) { repository.deleteById(id); } }

WidgetServiceImpladalah perkhidmatan Spring, diberi penjelasan dengan @Serviceanotasi, yang mempunyai WidgetRepositorykabel ke dalamnya melalui pembangunnya. Kaedah findById(), findAll()dan, deleteById()semuanya adalah kaedah penembusan yang mendasari WidgetRepository. Satu-satunya logik perniagaan yang anda dapati terdapat dalam save()kaedah ini, yang menambah bilangan versi Widgetapabila disimpan.

Kelas ujian

Untuk menguji kelas ini, kita perlu membuat dan mengkonfigurasi tiruan WidgetRepository, memasukkannya ke dalam WidgetServiceImplinstance, dan kemudian WidgetServiceImplmemasukkannya ke kelas ujian kita. Nasib baik, itu jauh lebih mudah daripada kedengarannya. Penyenaraian 3 menunjukkan kod sumber untuk WidgetServiceTestkelas.

Penyenaraian 3. Kelas ujian perkhidmatan Spring (WidgetServiceTest.java)

 package com.geekcap.javaworld.spring5mvcexample.service; import com.geekcap.javaworld.spring5mvcexample.model.Widget; import com.geekcap.javaworld.spring5mvcexample.repository.WidgetRepository; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.util.Arrays; import java.util.List; import java.util.Optional; import static org.mockito.Mockito.doReturn; import static org.mockito.ArgumentMatchers.any; @SpringBootTest public class WidgetServiceTest { /** * Autowire in the service we want to test */ @Autowired private WidgetService service; /** * Create a mock implementation of the WidgetRepository */ @MockBean private WidgetRepository repository; @Test @DisplayName("Test findById Success") void testFindById() { // Setup our mock repository Widget widget = new Widget(1l, "Widget Name", "Description", 1); doReturn(Optional.of(widget)).when(repository).findById(1l); // Execute the service call Optional returnedWidget = service.findById(1l); // Assert the response Assertions.assertTrue(returnedWidget.isPresent(), "Widget was not found"); Assertions.assertSame(returnedWidget.get(), widget, "The widget returned was not the same as the mock"); } @Test @DisplayName("Test findById Not Found") void testFindByIdNotFound() { // Setup our mock repository doReturn(Optional.empty()).when(repository).findById(1l); // Execute the service call Optional returnedWidget = service.findById(1l); // Assert the response Assertions.assertFalse(returnedWidget.isPresent(), "Widget should not be found"); } @Test @DisplayName("Test findAll") void testFindAll() { // Setup our mock repository Widget widget1 = new Widget(1l, "Widget Name", "Description", 1); Widget widget2 = new Widget(2l, "Widget 2 Name", "Description 2", 4); doReturn(Arrays.asList(widget1, widget2)).when(repository).findAll(); // Execute the service call List widgets = service.findAll(); // Assert the response Assertions.assertEquals(2, widgets.size(), "findAll should return 2 widgets"); } @Test @DisplayName("Test save widget") void testSave() { // Setup our mock repository Widget widget = new Widget(1l, "Widget Name", "Description", 1); doReturn(widget).when(repository).save(any()); // Execute the service call Widget returnedWidget = service.save(widget); // Assert the response Assertions.assertNotNull(returnedWidget, "The saved widget should not be null"); Assertions.assertEquals(2, returnedWidget.getVersion(), "The version should be incremented"); } } 

The WidgetServiceTest class is annotated with the @SpringBootTest annotation, which scans the CLASSPATH for all Spring configuration classes and beans and sets up the Spring application context for the test class. Note that WidgetServiceTest also implicitly includes the @ExtendWith(SpringExtension.class) annotation, through the @SpringBootTest annotation, which integrates the test class with JUnit 5.

The test class also uses Spring's @Autowired annotation to autowire a WidgetService to test against, and it uses Mockito's @MockBean annotation to create a mock WidgetRepository. At this point, we have a mock WidgetRepository that we can configure, and a real WidgetService with the mock WidgetRepository wired into it.

Testing the Spring service

Kaedah ujian pertama, testFindById(), bermaksud melaksanakan WidgetService's findById()kaedah, yang sepatutnya mengembalikan Optionalyang mengandungi Widget. Kita mulakan dengan membuat Widgetyang kita mahu WidgetRepositorykembali. Kami kemudian memanfaatkan API Mockito untuk mengkonfigurasi WidgetRepository::findByIdkaedah. Struktur logik olok-olok kami adalah seperti berikut:

 doReturn(VALUE_TO_RETURN).when(MOCK_CLASS_INSTANCE).MOCK_METHOD 

Dalam kes ini, kami mengatakan: Kembalikan salah satu Optionaldari kami Widgetketika kaedah repositori findById()dipanggil dengan argumen 1 (sebagai a long).

Seterusnya, kami mendapat ribuan WidgetService's findByIdkaedah dengan hujah daripada 1. Kami kemudian mengesahkan bahawa ia hadir dan bahawa kembali Widgetadalah salah satu yang kita dikonfigurasikan olok-olok yang WidgetRepositorypulangan.