Panduan Menguji Dinamik pada Junit 5

1. Gambaran keseluruhan

Pengujian dinamik adalah model pengaturcaraan baru yang diperkenalkan di JUnit 5. Dalam artikel ini, kita akan melihat apa sebenarnya ujian dinamik dan bagaimana membuatnya.

Sekiranya anda benar-benar baru menggunakan JUnit 5, anda mungkin ingin melihat pratonton JUnit 5 dan panduan utama kami.

2. Apa itu Ujian Dinamik ?

Ujian standard yang dianotasi dengan anotasi @Test adalah ujian statik yang dinyatakan sepenuhnya pada masa penyusunan. A DynamicTest adalah ujian yang dihasilkan semasa runtime . Ujian ini dihasilkan dengan kaedah kilang yang dijelaskan dengan anotasi @TestFactory .

A @TestFactory kaedah mesti mengembalikan Stream , Collection , Iterable , atau Pelelar daripada DynamicTest keadaan. Mengembalikan perkara lain akan menghasilkan JUnitException kerana jenis pengembalian yang tidak sah tidak dapat dikesan pada waktu kompilasi. Selain daripada ini, @TestFactory kaedah tidak boleh stati c atau swasta .

The DynamicTest s dilaksanakan secara berbeza daripada standard @Test s dan tidak menyokong panggilan balas kitaran hayat. Maksudnya, kaedah @BeforeEach dan @AfterEach tidak akan dipanggil untuk DynamicTest s .

3. Membuat Ujian Dinamik

Pertama, mari kita lihat pelbagai cara untuk membuat DynamicTest s.

Contoh di sini tidak bersifat dinamik, tetapi akan memberikan titik permulaan yang baik untuk membuat contoh yang benar-benar dinamik.

Kita akan membuat Collection of DynamicTest :

@TestFactory Collection dynamicTestsWithCollection() { return Arrays.asList( DynamicTest.dynamicTest("Add test", () -> assertEquals(2, Math.addExact(1, 1))), DynamicTest.dynamicTest("Multiply Test", () -> assertEquals(4, Math.multiplyExact(2, 2)))); }

The @TestFactory kaedah memberitahu JUnit bahawa ini adalah sebuah kilang untuk membuat ujian dinamik. Seperti yang kita dapat lihat, kita hanya mengembalikan Collection of DynamicTest . Setiap DynamicTest terdiri daripada dua bahagian, iaitu nama ujian atau nama paparan, dan sebuah Eksekusi .

Keluaran akan mengandungi nama paparan yang kami lalui untuk ujian dinamik:

Add test(dynamicTestsWithCollection()) Multiply Test(dynamicTestsWithCollection())

Ujian yang sama dapat diubah untuk mengembalikan Iterable , Iterator , atau Stream :

@TestFactory Iterable dynamicTestsWithIterable() { return Arrays.asList( DynamicTest.dynamicTest("Add test", () -> assertEquals(2, Math.addExact(1, 1))), DynamicTest.dynamicTest("Multiply Test", () -> assertEquals(4, Math.multiplyExact(2, 2)))); } @TestFactory Iterator dynamicTestsWithIterator() { return Arrays.asList( DynamicTest.dynamicTest("Add test", () -> assertEquals(2, Math.addExact(1, 1))), DynamicTest.dynamicTest("Multiply Test", () -> assertEquals(4, Math.multiplyExact(2, 2)))) .iterator(); } @TestFactory Stream dynamicTestsFromIntStream() { return IntStream.iterate(0, n -> n + 2).limit(10) .mapToObj(n -> DynamicTest.dynamicTest("test" + n, () -> assertTrue(n % 2 == 0))); }

Harap maklum bahawa jika @TestFactory mengembalikan Aliran , maka ia akan ditutup secara automatik setelah semua ujian dijalankan.

Hasilnya hampir sama dengan contoh pertama. Ini akan mengandungi nama paparan yang kami lalui untuk ujian dinamik.

4. Mewujudkan Stream of DynamicTests

Untuk tujuan demonstrasi, pertimbangkan DomainNameResolver yang mengembalikan alamat IP ketika kami memberikan nama domain sebagai input.

Demi kesederhanaan, mari kita lihat kerangka kaedah tinggi kilang kami:

@TestFactory Stream dynamicTestsFromStream() { // sample input and output List inputList = Arrays.asList( "www.somedomain.com", "www.anotherdomain.com", "www.yetanotherdomain.com"); List outputList = Arrays.asList( "154.174.10.56", "211.152.104.132", "178.144.120.156"); // input generator that generates inputs using inputList /*...code here...*/ // a display name generator that creates a // different name based on the input /*...code here...*/ // the test executor, which actually has the // logic to execute the test case /*...code here...*/ // combine everything and return a Stream of DynamicTest /*...code here...*/ }

Tidak banyak kod yang berkaitan dengan DynamicTest di sini selain daripada anotasi @TestFactory , yang sudah kita kenal.

Kedua ArrayList s akan digunakan sebagai input ke DomainNameResolver dan output yang diharapkan masing-masing.

Sekarang mari kita lihat penjana input:

Iterator inputGenerator = inputList.iterator();

Penjana input tidak lain adalah Iterator of String . Ia menggunakan inputList kami dan mengembalikan nama domain satu persatu.

Penjana nama paparan agak mudah:

Function displayNameGenerator = (input) -> "Resolving: " + input;

Tugas penjana nama paparan hanya untuk memberikan nama paparan untuk kes ujian yang akan digunakan dalam laporan JUnit atau tab JUnit IDE kami.

Di sini kita hanya menggunakan nama domain untuk menghasilkan nama yang unik untuk setiap ujian. Tidak perlu membuat nama yang unik, tetapi akan membantu sekiranya berlaku kegagalan. Setelah ini, kami dapat memberitahu nama domain yang kes ujiannya gagal.

Sekarang mari kita lihat bahagian tengah ujian kami - kod pelaksanaan ujian:

DomainNameResolver resolver = new DomainNameResolver(); ThrowingConsumer testExecutor = (input) -> { int id = inputList.indexOf(input); assertEquals(outputList.get(id), resolver.resolveDomain(input)); };

Kami telah menggunakan ThrowingConsumer , yang merupakan @FunctionalInterface untuk menulis kes ujian. Untuk setiap input yang dihasilkan oleh penjana data, kami mengambil output yang diharapkan dari outputList dan output sebenar dari instance DomainNameResolver .

Sekarang bahagian terakhir adalah untuk mengumpulkan semua kepingan dan kembali sebagai Stream of DynamicTest :

return DynamicTest.stream( inputGenerator, displayNameGenerator, testExecutor);

Itu sahaja. Menjalankan ujian akan memaparkan laporan yang mengandungi nama yang ditentukan oleh penjana nama paparan kami:

Resolving: www.somedomain.com(dynamicTestsFromStream()) Resolving: www.anotherdomain.com(dynamicTestsFromStream()) Resolving: www.yetanotherdomain.com(dynamicTestsFromStream())

5. Meningkatkan DynamicTest Menggunakan Ciri Java 8

Kilang ujian yang ditulis di bahagian sebelumnya dapat diperbaiki secara drastik dengan menggunakan ciri-ciri Java 8. Kod yang dihasilkan akan jauh lebih bersih dan dapat ditulis dalam jumlah baris yang lebih sedikit:

@TestFactory Stream dynamicTestsFromStreamInJava8() { DomainNameResolver resolver = new DomainNameResolver(); List domainNames = Arrays.asList( "www.somedomain.com", "www.anotherdomain.com", "www.yetanotherdomain.com"); List outputList = Arrays.asList( "154.174.10.56", "211.152.104.132", "178.144.120.156"); return inputList.stream() .map(dom -> DynamicTest.dynamicTest("Resolving: " + dom, () -> {int id = inputList.indexOf(dom); assertEquals(outputList.get(id), resolver.resolveDomain(dom)); })); }

Kod di atas mempunyai kesan yang sama seperti yang kita lihat di bahagian sebelumnya. The inputList.stream (). Map () menyediakan aliran input (penjana input). Argumen pertama untuk dynamicTest () adalah penjana nama paparan kami ("Menyelesaikan:" + dom ) sementara argumen kedua, lambda , adalah pelaksana ujian kami.

Keluarannya akan sama seperti yang dikeluarkan dari bahagian sebelumnya.

6. Contoh Tambahan

Dalam contoh ini, kami meneroka lebih jauh kekuatan ujian dinamik untuk menyaring input berdasarkan kes ujian:

@TestFactory Stream dynamicTestsForEmployeeWorkflows() { List inputList = Arrays.asList( new Employee(1, "Fred"), new Employee(2), new Employee(3, "John")); EmployeeDao dao = new EmployeeDao(); Stream saveEmployeeStream = inputList.stream() .map(emp -> DynamicTest.dynamicTest( "saveEmployee: " + emp.toString(), () -> { Employee returned = dao.save(emp.getId()); assertEquals(returned.getId(), emp.getId()); } )); Stream saveEmployeeWithFirstNameStream = inputList.stream() .filter(emp -> !emp.getFirstName().isEmpty()) .map(emp -> DynamicTest.dynamicTest( "saveEmployeeWithName" + emp.toString(), () -> { Employee returned = dao.save(emp.getId(), emp.getFirstName()); assertEquals(returned.getId(), emp.getId()); assertEquals(returned.getFirstName(), emp.getFirstName()); })); return Stream.concat(saveEmployeeStream, saveEmployeeWithFirstNameStream); }

The save(Long) method needs only the employeeId. Hence, it utilizes all the Employee instances. The save(Long, String) method needs firstName apart from the employeeId. Hence, it filters out the Employee instances without firstName.

Finally, we combine both the streams and return all the tests as a single Stream.

Now, let's have a look at the output:

saveEmployee: Employee [id=1, firstName=Fred](dynamicTestsForEmployeeWorkflows()) saveEmployee: Employee [id=2, firstName=](dynamicTestsForEmployeeWorkflows()) saveEmployee: Employee [id=3, firstName=John](dynamicTestsForEmployeeWorkflows()) saveEmployeeWithNameEmployee [id=1, firstName=Fred](dynamicTestsForEmployeeWorkflows()) saveEmployeeWithNameEmployee [id=3, firstName=John](dynamicTestsForEmployeeWorkflows())

7. Conclusion

The parameterized tests can replace many of the examples in this article. However, the dynamic tests differ from the parameterized tests as they support full test lifecycle, while parametrized tests don't.

Lebih-lebih lagi, ujian dinamik memberikan lebih banyak fleksibiliti mengenai bagaimana input dihasilkan dan bagaimana ujian dijalankan.

JUnit 5 lebih suka peluasan daripada prinsip ciri. Akibatnya, tujuan utama ujian dinamik adalah untuk memberikan titik peluasan untuk kerangka atau peluasan pihak ketiga.

Anda boleh membaca lebih lanjut mengenai ciri JUnit 5 yang lain dalam artikel kami mengenai ujian berulang di JUnit 5.

Jangan lupa untuk melihat kod sumber penuh artikel ini di GitHub.