Tutorial JUnit 5, bagian 2: Pengujian unit MVC Musim Semi dengan JUnit 5

Spring MVC adalah salah satu framework Java paling populer untuk membangun aplikasi Java perusahaan, dan cocok untuk pengujian. Secara desain, Spring MVC mempromosikan pemisahan masalah dan mendorong pengkodean terhadap antarmuka. Kualitas ini, bersama dengan implementasi injeksi ketergantungan Spring, membuat aplikasi Spring sangat dapat diuji.

Tutorial ini adalah paruh kedua dari pengantar saya untuk pengujian unit dengan JUnit 5. Saya akan menunjukkan cara mengintegrasikan JUnit 5 dengan Spring, kemudian memperkenalkan Anda ke tiga alat yang dapat Anda gunakan untuk menguji pengontrol, layanan, dan repositori MVC Spring.

download Dapatkan kode Download kode sumber untuk aplikasi contoh yang digunakan dalam tutorial ini. Dibuat oleh Steven Haines untuk JavaWorld.

Mengintegrasikan JUnit 5 dengan Spring 5

Untuk tutorial ini, kami menggunakan Maven dan Spring Boot, jadi hal pertama yang perlu kami lakukan adalah menambahkan dependensi JUnit 5 ke file POM Maven kami:

  org.junit.jupiter junit-jupiter 5.6.0 test  

Sama seperti yang kita lakukan di Bagian 1, kita akan menggunakan Mockito untuk contoh ini. Jadi, kita perlu menambahkan perpustakaan JUnit 5 Mockito:

  org.mockito mockito-junit-jupiter 3.2.4 test  

@ExtendWith dan kelas SpringExtension

JUnit 5 mendefinisikan antarmuka ekstensi , yang melaluinya kelas dapat berintegrasi dengan pengujian JUnit pada berbagai tahapan siklus hidup eksekusi. Kita dapat mengaktifkan ekstensi dengan menambahkan @ExtendWithanotasi ke kelas pengujian kita dan menentukan kelas ekstensi yang akan dimuat. Ekstensi kemudian dapat mengimplementasikan berbagai antarmuka callback, yang akan dipanggil sepanjang siklus hidup pengujian: sebelum semua pengujian dijalankan, sebelum setiap pengujian dijalankan, setelah setiap pengujian dijalankan, dan setelah semua pengujian dijalankan.

Spring mendefinisikan SpringExtensionkelas yang berlangganan notifikasi siklus proses JUnit 5 untuk membuat dan memelihara "konteks pengujian". Ingat bahwa konteks aplikasi Spring berisi semua kacang Spring dalam sebuah aplikasi dan melakukan injeksi ketergantungan untuk menyatukan aplikasi dan dependensinya. Spring menggunakan model ekstensi JUnit 5 untuk mempertahankan konteks aplikasi pengujian, yang membuat pengujian unit penulisan dengan Spring menjadi mudah.

Setelah kami menambahkan pustaka JUnit 5 ke file POM Maven kami, kami dapat menggunakan SpringExtension.classuntuk memperluas kelas pengujian JUnit 5 kami:

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

Contohnya dalam hal ini adalah aplikasi Spring Boot. Untungnya @SpringBootTestanotasi tersebut sudah menyertakan @ExtendWith(SpringExtension.class)anotasi tersebut, jadi kita hanya perlu menyertakannya @SpringBootTest.

Menambahkan ketergantungan Mockito

Untuk menguji setiap komponen dengan benar dalam isolasi dan mensimulasikan skenario yang berbeda, kita akan membuat implementasi tiruan dari dependensi setiap kelas. Di sinilah Mockito masuk. Sertakan ketergantungan berikut dalam file POM Anda untuk menambahkan dukungan untuk Mockito:

  org.mockito mockito-junit-jupiter 3.2.4 test  

Setelah Anda mengintegrasikan JUnit 5 dan Mockito ke dalam aplikasi Spring Anda, Anda dapat memanfaatkan Mockito hanya dengan mendefinisikan kacang Spring (seperti layanan atau repositori) di kelas pengujian 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 tiruan WidgetRepositorydi dalam WidgetServiceTestkelas kami . Ketika Spring melihat ini, itu akan secara otomatis menghubungkannya ke kami WidgetServicesehingga kami dapat membuat skenario yang berbeda dalam metode pengujian kami. Setiap metode pengujian akan mengonfigurasi perilaku WidgetRepository, seperti dengan mengembalikan yang diminta Widgetatau mengembalikan Optional.empty()kueri yang datanya tidak ditemukan. Kami akan menghabiskan sisa tutorial ini melihat contoh berbagai cara untuk mengkonfigurasi kacang tiruan ini.

Aplikasi contoh Spring MVC

Untuk menulis pengujian unit berbasis Spring, kita membutuhkan aplikasi untuk menulisnya. Untungnya, kita dapat menggunakan aplikasi contoh dari tutorial Spring Series saya "Menguasai kerangka kerja Musim Semi 5, Bagian 1: MVC Musim Semi." Saya menggunakan aplikasi contoh dari tutorial itu sebagai aplikasi dasar. Saya memodifikasinya dengan REST API yang lebih kuat sehingga kami memiliki beberapa hal lagi untuk diuji.

Contoh aplikasi adalah aplikasi web Spring MVC dengan pengontrol REST, lapisan layanan, dan repositori yang menggunakan Spring Data JPA untuk menyimpan "widget" ke dan dari database dalam memori H2. Gambar 1 adalah gambaran umum.

Steven Haines

Apa itu widget?

A Widgethanyalah "benda" dengan ID, nama, deskripsi, dan nomor versi. Dalam hal ini, widget kami dianotasi dengan anotasi JPA untuk mendefinisikannya sebagai entitas. Ini WidgetRestControlleradalah pengontrol MVC Spring yang menerjemahkan panggilan RESTful API menjadi tindakan untuk dijalankan Widgets. Ini WidgetServiceadalah layanan Spring standar yang menentukan fungsionalitas bisnis untuk Widgets. Terakhir, WidgetRepositoryadalah antarmuka JPA Data Musim Semi, di mana Spring akan membuat implementasi saat runtime. Kami akan meninjau kode untuk setiap kelas saat kami menulis tes di bagian selanjutnya.

Unit menguji layanan Spring

Mari kita mulai dengan meninjau cara menguji layanan Spring  , karena itu adalah komponen termudah dalam aplikasi MVC kita untuk diuji. Contoh di bagian ini akan memungkinkan kita untuk menjelajahi integrasi JUnit 5 dengan Spring tanpa memperkenalkan komponen atau pustaka pengujian baru, meskipun kita akan melakukannya nanti di tutorial.

Kami akan mulai dengan meninjau WidgetServiceantarmuka dan WidgetServiceImplkelas, yang masing-masing ditunjukkan pada Listing 1 dan Listing 2.

Daftar 1. Antarmuka layanan 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); }

Kode 2. Kelas implementasi layanan 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 layanan Spring, dianotasi dengan @Serviceanotasi, yang memiliki WidgetRepositorykabel ke dalamnya melalui konstruktornya. Semua findById(),, findAll()dan deleteById()metode adalah metode masuk ke yang mendasarinya WidgetRepository. Satu-satunya logika bisnis yang akan Anda temukan terletak di save()metode, yang menambah nomor versi Widgetketika disimpan.

Kelas uji

Untuk menguji kelas ini, kita perlu membuat dan mengonfigurasi tiruan WidgetRepository, menyambungkannya ke dalam WidgetServiceImplinstance, dan kemudian menyambungkannya WidgetServiceImplke kelas uji kita. Untungnya, itu jauh lebih mudah daripada kedengarannya. Kode 3 menunjukkan kode sumber untuk WidgetServiceTestkelas.

Kode 3. Kelas uji layanan 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 WidgetServiceTestkelas dijelaskan dengan @SpringBootTestpenjelasan, yang memindai CLASSPATHuntuk semua kelas konfigurasi Spring dan kacang-kacangan dan set up konteks aplikasi Spring untuk kelas uji. Perhatikan bahwa WidgetServiceTestjuga secara implisit menyertakan @ExtendWith(SpringExtension.class)anotasi, melalui @SpringBootTestanotasi, yang mengintegrasikan kelas pengujian dengan JUnit 5.

Kelas pengujian juga menggunakan @Autowiredanotasi Spring untuk melakukan autowire a WidgetServiceuntuk diuji, dan menggunakan @MockBeananotasi Mockito untuk membuat tiruan WidgetRepository. Pada titik ini, kami memiliki tiruan WidgetRepositoryyang dapat kami konfigurasi, dan asli WidgetServicedengan tiruan yang WidgetRepositorydihubungkan ke dalamnya.

Menguji layanan Spring

Metode tes pertama, testFindById(), mengeksekusi WidgetService's findById()metode, yang harus mengembalikan Optionalyang berisi Widget. Kami mulai dengan membuat Widgetyang kami ingin WidgetRepositorykembalikan. Kami kemudian memanfaatkan Mockito API untuk mengonfigurasi WidgetRepository::findByIdmetode. Struktur logika tiruan kami adalah sebagai berikut:

 doReturn(VALUE_TO_RETURN).when(MOCK_CLASS_INSTANCE).MOCK_METHOD 

Dalam kasus ini, kami mengatakan: Kembalikan Optionaldari kami Widgetsaat metode repositori findById()dipanggil dengan argumen 1 (sebagai a long).

Berikutnya, kita memohon WidgetService's findByIdmetode dengan argumen dari 1. Kami kemudian memvalidasi bahwa itu hadir dan bahwa kembali Widgetadalah salah satu yang kita dikonfigurasi pura-pura WidgetRepositoryuntuk kembali.