Saya memutuskan untuk membagikan visi saya tentang pengujian unit berparameter, bagaimana kami melakukannya, dan bagaimana Anda mungkin tidak melakukannya (tetapi ingin melakukannya).
Saya ingin menulis ungkapan yang indah tentang apa yang harus diuji dengan benar, dan tes itu penting, tetapi banyak materi telah dikatakan dan ditulis sebelum saya, saya hanya akan mencoba merangkum dan menyoroti apa, menurut saya, orang jarang menggunakan (mengerti), yang pada dasarnya pindah.
Tujuan utama artikel ini adalah untuk menunjukkan bagaimana Anda dapat (dan seharusnya) menghentikan pengujian unit yang mengacaukan dengan kode untuk membuat objek, dan cara membuat data pengujian secara deklaratif jika tiruan (any ()) tidak cukup, dan terdapat banyak situasi seperti itu.
Mari buat proyek maven, tambahkan junit5, junit-jupiter-params, dan mokito ke dalamnya.
Agar tidak membosankan, kita akan langsung mulai menulis dari ujian, seperti yang disukai oleh para pembela TDD, kita memerlukan layanan yang akan kita uji secara deklaratif, apa pun yang akan dilakukan, biarlah HabrService.
Mari buat tes HabrServiceTest. Tambahkan link ke HabrService di kolom kelas pengujian:
public class HabrServiceTest {
private HabrService habrService;
@Test
void handleTest(){
}
}
buat layanan melalui ide (dengan menekan pintasan ringan), tambahkan anotasi @InjectMocks ke bidang.
Mari kita mulai langsung dengan pengujian: HabrService dalam aplikasi kecil kita akan memiliki satu metode handle () yang akan mengambil satu argumen HabrItem, dan sekarang pengujian kita terlihat seperti ini:
public class HabrServiceTest {
@InjectMocks
private HabrService habrService;
@Test
void handleTest(){
HabrItem item = new HabrItem();
habrService.handle(item);
}
}
Mari tambahkan metode handle () ke HabrService, yang akan mengembalikan id dari posting baru di Habré setelah itu dimoderasi dan disimpan ke database, dan mengambil tipe HabrItem, kita juga akan membuat HabrItem kita, dan sekarang tes mengkompilasi, tetapi crash.
Intinya adalah kita menambahkan cek untuk nilai pengembalian yang diharapkan.
public class HabrServiceTest {
@InjectMocks
private HabrService habrService;
@BeforeEach
void setUp(){
initMocks(this);
}
@Test
void handleTest() {
HabrItem item = new HabrItem();
Long actual = habrService.handle(item);
assertEquals(1L, actual);
}
}
Juga, saya ingin memastikan bahwa selama panggilan ke metode handle (), ReviewService dan PersistanceService dipanggil, mereka dipanggil secara ketat satu demi satu, mereka bekerja tepat 1 kali, dan tidak ada metode lain yang dipanggil lagi. Dengan kata lain, seperti ini:
public class HabrServiceTest {
@InjectMocks
private HabrService habrService;
@BeforeEach
void setUp(){
initMocks(this);
}
@Test
void handleTest() {
HabrItem item = new HabrItem();
Long actual = habrService.handle(item);
InOrder inOrder = Mockito.inOrder(reviewService, persistenceService);
inOrder.verify(reviewService).makeRewiew(item);
inOrder.verify(persistenceService).makePersist(item);
inOrder.verifyNoMoreInteractions();
assertEquals(1L, actual);
}
}
Tambahkan reviewService dan persistenceService ke bidang kelas kelas, buat mereka, tambahkan metode makeRewiew () dan makePersist () ke bidang tersebut. Sekarang semuanya terkompilasi, tetapi tentu saja tesnya berwarna merah.
Dalam konteks artikel ini, implementasi ReviewService dan PersistanceService tidak begitu penting, implementasi HabrService itu penting, mari kita buat sedikit lebih menarik daripada sekarang:
public class HabrService {
private final ReviewService reviewService;
private final PersistenceService persistenceService;
public HabrService(final ReviewService reviewService, final PersistenceService persistenceService) {
this.reviewService = reviewService;
this.persistenceService = persistenceService;
}
public Long handle(final HabrItem item) {
HabrItem reviewedItem = reviewService.makeRewiew(item);
Long persistedItemId = persistenceService.makePersist(reviewedItem);
return persistedItemId;
}
}
dan menggunakan konstruksi when (). then () kita mengunci perilaku komponen tambahan, sebagai hasilnya, pengujian kita menjadi seperti ini dan sekarang menjadi hijau:
public class HabrServiceTest {
@Mock
private ReviewService reviewService;
@Mock
private PersistenceService persistenceService;
@InjectMocks
private HabrService habrService;
@BeforeEach
void setUp() {
initMocks(this);
}
@Test
void handleTest() {
HabrItem source = new HabrItem();
HabrItem reviewedItem = mock(HabrItem.class);
when(reviewService.makeRewiew(source)).thenReturn(reviewedItem);
when(persistenceService.makePersist(reviewedItem)).thenReturn(1L);
Long actual = habrService.handle(source);
InOrder inOrder = Mockito.inOrder(reviewService, persistenceService);
inOrder.verify(reviewService).makeRewiew(source);
inOrder.verify(persistenceService).makePersist(reviewedItem);
inOrder.verifyNoMoreInteractions();
assertEquals(1L, actual);
}
}
Maket untuk mendemonstrasikan kekuatan pengujian berparameter sudah siap.
Mari tambahkan bidang dengan tipe hub, hubType ke model permintaan kami untuk layanan HabrItem, buat enum HubType dan sertakan beberapa tipe di dalamnya:
public enum HubType {
JAVA, C, PYTHON
}
dan untuk model HabrItem, tambahkan pengambil dan penyetel ke bidang HubType yang dibuat.
Misalkan sakelar tersembunyi di kedalaman HabrService kami, yang, bergantung pada jenis hub, melakukan sesuatu yang tidak diketahui dengan permintaan tersebut, dan dalam pengujian kami ingin menguji setiap kasus yang tidak diketahui, implementasi metode yang naif akan terlihat seperti ini:
@Test
void handleTest() {
HabrItem reviewedItem = mock(HabrItem.class);
HabrItem source = new HabrItem();
source.setHubType(HubType.JAVA);
when(reviewService.makeRewiew(source)).thenReturn(reviewedItem);
when(persistenceService.makePersist(reviewedItem)).thenReturn(1L);
Long actual = habrService.handle(source);
InOrder inOrder = Mockito.inOrder(reviewService, persistenceService);
inOrder.verify(reviewService).makeRewiew(source);
inOrder.verify(persistenceService).makePersist(reviewedItem);
inOrder.verifyNoMoreInteractions();
assertEquals(1L, actual);
}
Anda dapat membuatnya sedikit lebih cantik dan nyaman dengan membuat parameter pengujian dan menambahkan nilai acak dari enum kami sebagai parameter, sebagai hasilnya, deklarasi pengujian akan terlihat seperti ini:
@ParameterizedTest
@EnumSource(HubType.class)
void handleTest(final HubType type)
baik, secara deklaratif, dan semua nilai enum kita pasti akan digunakan pada beberapa pengujian berikutnya, anotasi memiliki parameter, kita dapat menambahkan strategi untuk memasukkan, mengecualikan.
Tetapi mungkin saya belum meyakinkan Anda bahwa pengujian berparameter itu bagus. Tambahkan
permintaan HabrItem asli adalah bidang editCount baru, di mana ribuan kali pengguna Habr mengedit artikel mereka akan ditulis sebelum memposting, sehingga Anda setidaknya menyukainya, dan anggap bahwa di suatu tempat di kedalaman HabrService ada semacam logika yang melakukan hal yang tidak diketahui sesuatu, tergantung pada seberapa banyak penulis mencoba, bagaimana jika saya tidak ingin menulis 5 atau 55 tes untuk semua kemungkinan opsi editCount, tetapi saya ingin menguji secara deklaratif, dan di suatu tempat di satu tempat segera menunjukkan semua nilai yang ingin saya periksa ... Tidak ada yang lebih sederhana, dan dengan menggunakan api pengujian berparameter, kita mendapatkan sesuatu seperti ini di deklarasi metode:
@ParameterizedTest
@ValueSource(ints = {0, 5, 14, 23})
void handleTest(final int type)
Ada masalah, kami ingin mengumpulkan dua nilai secara deklaratif dalam parameter metode pengujian sekaligus, Anda dapat menggunakan metode lain yang sangat baik dari pengujian berparameter @CsvSource, cocok untuk menguji parameter sederhana, dengan nilai keluaran sederhana (sangat nyaman untuk menguji kelas utilitas), tapi apa jika objek menjadi jauh lebih rumit? Katakanlah itu akan memiliki sekitar 10 bidang, dan tidak hanya primitif dan tipe Java.
Anotasi @MethodSource datang untuk menyelamatkan, metode pengujian kami menjadi jauh lebih pendek dan tidak ada lagi penyetel di dalamnya, dan sumber permintaan masuk diumpankan ke metode pengujian sebagai parameter:
@ParameterizedTest
@MethodSource("generateSource")
void handleTest(final HabrItem source) {
HabrItem reviewedItem = mock(HabrItem.class);
when(reviewService.makeRewiew(source)).thenReturn(reviewedItem);
when(persistenceService.makePersist(reviewedItem)).thenReturn(1L);
Long actual = habrService.handle(source);
InOrder inOrder = Mockito.inOrder(reviewService, persistenceService);
inOrder.verify(reviewService).makeRewiew(source);
inOrder.verify(persistenceService).makePersist(reviewedItem);
inOrder.verifyNoMoreInteractions();
assertEquals(1L, actual);
}
anotasi @MethodSource memiliki string generateSource, apakah itu? ini adalah nama metode yang akan mengumpulkan model yang diperlukan untuk kita, deklarasinya akan terlihat seperti ini:
private static Stream<Arguments> generateSource() {
HabrItem habrItem = new HabrItem();
habrItem.setHubType(HubType.JAVA);
habrItem.setEditCount(999L);
return nextStream(() -> habrItem);
}
Untuk kenyamanan, saya memindahkan formasi aliran argumen nextStream ke dalam kelas uji utilitas terpisah:
public class CommonTestUtil {
private static final Random RANDOM = new Random();
public static <T> Stream<Arguments> nextStream(final Supplier<T> supplier) {
return Stream.generate(() -> Arguments.of(supplier.get())).limit(nextIntBetween(1, 10));
}
public static int nextIntBetween(final int min, final int max) {
return RANDOM.nextInt(max - min + 1) + min;
}
}
Sekarang, saat memulai pengujian, model permintaan HabrItem akan ditambahkan secara deklaratif ke parameter metode pengujian, dan pengujian akan diluncurkan sebanyak jumlah argumen yang dihasilkan oleh utilitas pengujian kami, dalam kasus kami dari 1 hingga 10.
Ini bisa sangat nyaman jika model berada dalam aliran argumen dikumpulkan bukan dengan hardcode, seperti dalam contoh kami, tetapi dengan bantuan pengacak. (Pengujian mengambang jangka panjang, tetapi jika ada, ada masalah).
Menurut saya, semuanya sudah super, tes sekarang hanya menjelaskan perilaku rintisan kita, dan hasil yang diharapkan.
Tapi inilah nasib buruknya, bidang baru, teks, serangkaian string ditambahkan ke model HabrItem, yang mungkin sangat besar atau tidak, tidak masalah, yang utama adalah kami tidak ingin mengacaukan pengujian kami, kami tidak memerlukan data acak, kami menginginkan model yang ditentukan secara ketat, dengan data tertentu, mengumpulkannya dalam ujian atau di mana pun - kami tidak ingin. Akan keren jika Anda dapat mengambil isi permintaan json dari mana saja, misalnya dari tukang pos, membuat file tiruan berdasarkan itu, dan membentuk model secara deklaratif dalam pengujian, hanya menentukan jalur ke file json dengan data.
Luar biasa. Kami menggunakan anotasi @JsonSource, yang akan mengambil parameter jalur, dengan jalur file relatif dan kelas target. Heck! Tidak ada anotasi seperti itu dalam pengujian berparameter, tapi saya ingin.
Ayo tulis sendiri.
ArgumentsProvider bertanggung jawab untuk memproses semua anotasi yang disertakan dengan @ParametrizedTest di junit, kita akan menulis JsonArgumentProvider kita sendiri:
public class JsonArgumentProvider implements ArgumentsProvider, AnnotationConsumer<JsonSource> {
private String path;
private MockDataProvider dataProvider;
private Class<?> clazz;
@Override
public void accept(final JsonSource jsonSource) {
this.path = jsonSource.path();
this.dataProvider = new MockDataProvider(new ObjectMapper());
this.clazz = jsonSource.clazz();
}
@Override
public Stream<Arguments> provideArguments(final ExtensionContext context) {
return nextSingleStream(() -> dataProvider.parseDataObject(path, clazz));
}
}
MockDataProvider adalah kelas untuk parsing file json tiruan, implementasinya sangat sederhana:
public class MockDataProvider {
private static final String PATH_PREFIX = "json/";
private final ObjectMapper objectMapper;
public <T> T parseDataObject(final String name, final Class<T> clazz) {
return objectMapper.readValue(new ClassPathResource(PATH_PREFIX + name).getInputStream(), clazz);
}
}
Penyedia tiruan sudah siap, penyedia argumen untuk anotasi kita juga, tetap menambahkan anotasi itu sendiri:
/**
* Source- ,
* json-
*/
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ArgumentsSource(JsonArgumentProvider.class)
public @interface JsonSource {
/**
* json-, classpath:/json/
*
* @return
*/
String path() default "";
/**
* ,
*
* @return
*/
Class<?> clazz();
}
Hore. Anotasi kami siap digunakan, metode pengujiannya sekarang:
@ParameterizedTest
@JsonSource(path = MOCK_FILE_PATH, clazz = HabrItem.class)
void handleTest(final HabrItem source) {
HabrItem reviewedItem = mock(HabrItem.class);
when(reviewService.makeRewiew(source)).thenReturn(reviewedItem);
when(persistenceService.makePersist(reviewedItem)).thenReturn(1L);
Long actual = habrService.handle(source);
InOrder inOrder = Mockito.inOrder(reviewService, persistenceService);
inOrder.verify(reviewService).makeRewiew(source);
inOrder.verify(persistenceService).makePersist(reviewedItem);
inOrder.verifyNoMoreInteractions();
assertEquals(1L, actual);
}
di mock json, kami dapat menghasilkan sebanyak dan sangat cepat sekumpulan objek yang kami butuhkan, dan mulai sekarang tidak ada kode yang mengalihkan perhatian dari esensi pengujian, untuk pembentukan data pengujian, tentu saja, Anda sering dapat melakukannya dengan tiruan, tetapi tidak selalu.
Kesimpulannya, saya ingin mengatakan yang berikut: seringkali kita bekerja seperti dulu bekerja, selama bertahun-tahun, tanpa berpikir bahwa beberapa hal dapat dilakukan dengan indah dan sederhana, seringkali menggunakan api standar dari perpustakaan yang telah kita gunakan selama bertahun-tahun, tetapi tidak mengetahui semua kemampuannya.
PS Artikel ini bukan merupakan upaya untuk pengetahuan tentang konsep TDD, saya ingin menambahkan data pengujian ke kampanye mendongeng agar sedikit lebih jelas dan lebih menarik.