Perbandingan JUnit Pantas vs TestNG

1. Gambaran keseluruhan

JUnit dan TestNG tidak diragukan lagi adalah dua kerangka pengujian unit paling popular di ekosistem Java. Walaupun JUnit memberi inspirasi kepada TestNG itu sendiri, ia memberikan ciri khasnya, dan tidak seperti JUnit, ia berfungsi untuk tahap pengujian yang berfungsi dan lebih tinggi.

Dalam catatan ini, kita akan membincangkan dan membandingkan kerangka kerja ini dengan merangkumi ciri dan kes penggunaan biasa mereka .

2. Persediaan Ujian

Semasa menulis kes ujian, seringkali kita perlu melaksanakan beberapa arahan konfigurasi atau inisialisasi sebelum pelaksanaan ujian, dan juga beberapa pembersihan setelah menyelesaikan ujian. Mari kita menilai perkara ini dalam kedua-dua kerangka tersebut.

JUnit menawarkan inisialisasi dan pembersihan pada dua tahap, sebelum dan selepas setiap kaedah dan kelas. Kami mempunyai @BeforeEach , @AfterEach penjelasan di peringkat kaedah dan @BeforeAll dan @AfterAll di peringkat kelas:

public class SummationServiceTest { private static List numbers; @BeforeAll public static void initialize() { numbers = new ArrayList(); } @AfterAll public static void tearDown() { numbers = null; } @BeforeEach public void runBeforeEachTest() { numbers.add(1); numbers.add(2); numbers.add(3); } @AfterEach public void runAfterEachTest() { numbers.clear(); } @Test public void givenNumbers_sumEquals_thenCorrect() { int sum = numbers.stream().reduce(0, Integer::sum); assertEquals(6, sum); } }

Perhatikan bahawa contoh ini kegunaan Junit 5. sebelum versi JUnit 4, kita akan perlu menggunakan @Before dan @After penjelasan yang bersamaan dengan @BeforeEach dan @AfterEach. Begitu juga, @BeforeAll dan @AfterAll adalah pengganti untuk JUnit 4's @BeforeClass dan @AfterClass.

Sama seperti JUnit, TestNG juga menyediakan inisialisasi dan pembersihan pada tahap kaedah dan kelas . Walaupun @BeforeClass dan @AfterClass tetap sama di peringkat kelas, anotasi tahap kaedah adalah @ BeforeMethod dan @AfterMethod :

@BeforeClass public void initialize() { numbers = new ArrayList(); } @AfterClass public void tearDown() { numbers = null; } @BeforeMethod public void runBeforeEachTest() { numbers.add(1); numbers.add(2); numbers.add(3); } @AfterMethod public void runAfterEachTest() { numbers.clear(); }

TestNG juga menawarkan anotasi , @BeforeSuite, @AfterSuite, @BeforeGroup dan @AfterGroup , untuk konfigurasi di peringkat suite dan kumpulan:

@BeforeGroups("positive_tests") public void runBeforeEachGroup() { numbers.add(1); numbers.add(2); numbers.add(3); } @AfterGroups("negative_tests") public void runAfterEachGroup() { numbers.clear(); }

Juga, kita boleh menggunakan @BeforeTest dan @ AfterTest jika kita memerlukan konfigurasi sebelum atau selepas kes ujian termasuk dalamtag dalam fail konfigurasi TestNG XML:

Perhatikan bahawa pengisytiharan @BeforeClass dan @AfterClass kaedah ini boleh menjadi statik dalam JUnit. Sebagai perbandingan, pengisytiharan kaedah TestNG tidak mempunyai kekangan ini.

3. Mengabaikan Ujian

Kedua-dua kerangka ini menyokong mengabaikan kes ujian , walaupun mereka melakukannya dengan cara yang berbeza. JUnit menawarkan anotasi @Ignore :

@Ignore @Test public void givenNumbers_sumEquals_thenCorrect() { int sum = numbers.stream().reduce(0, Integer::sum); Assert.assertEquals(6, sum); }

sementara TestNG menggunakan @Test dengan parameter "diaktifkan" dengan nilai boolean benar atau salah :

@Test(enabled=false) public void givenNumbers_sumEquals_thenCorrect() { int sum = numbers.stream.reduce(0, Integer::sum); Assert.assertEquals(6, sum); }

4. Menjalankan Ujian Bersama

Menjalankan ujian bersama-sama sebagai koleksi adalah mungkin di JUnit dan TestNG, tetapi mereka melakukannya dengan cara yang berbeza.

Kita boleh menggunakan anotasi @RunWith, @SelectPackages , dan @SelectClasses untuk mengumpulkan kes ujian dan menjalankannya sebagai suite di JUnit 5 . Suite adalah kumpulan kes ujian yang dapat kita kumpulkan bersama dan dijalankan sebagai satu ujian.

Sekiranya kami ingin mengumpulkan kes ujian pakej yang berlainan untuk dijalankan bersama dalam Suite, kami memerlukan anotasi @SelectPackages :

@RunWith(JUnitPlatform.class) @SelectPackages({ "org.baeldung.java.suite.childpackage1", "org.baeldung.java.suite.childpackage2" }) public class SelectPackagesSuiteUnitTest { }

Sekiranya kami mahukan kelas ujian tertentu dijalankan bersama, JUnit 5 memberikan kelonggaran melalui @SelectClasses :

@RunWith(JUnitPlatform.class) @SelectClasses({Class1UnitTest.class, Class2UnitTest.class}) public class SelectClassesSuiteUnitTest { }

Sebelumnya menggunakan JUnit 4 , kami berjaya mengumpulkan dan menjalankan beberapa ujian bersama-sama menggunakan anotasi @Suite :

@RunWith(Suite.class) @Suite.SuiteClasses({ RegistrationTest.class, SignInTest.class }) public class SuiteTest { }

Di TestNG kita dapat mengumpulkan ujian dengan menggunakan fail XML:

Ini menunjukkan RegistrationTest dan SignInTest akan berjalan bersama.

Selain daripada kelas pengelompokan, TestNG juga dapat mengumpulkan kaedah menggunakan anotasi @ Test (kumpulan = "groupName") :

@Test(groups = "regression") public void givenNegativeNumber_sumLessthanZero_thenCorrect() { int sum = numbers.stream().reduce(0, Integer::sum); Assert.assertTrue(sum < 0); }

Mari gunakan XML untuk melaksanakan kumpulan:

Ini akan melaksanakan kaedah ujian yang ditandai dengan regresi kumpulan .

5. Pengecualian Menguji

Ciri untuk menguji pengecualian menggunakan anotasi tersedia di JUnit dan TestNG.

Mari buat kelas terlebih dahulu dengan kaedah yang memberikan pengecualian:

public class Calculator { public double divide(double a, double b) { if (b == 0) { throw new DivideByZeroException("Divider cannot be equal to zero!"); } return a/b; } }

Di JUnit 5 kita dapat menggunakan assertThrows API untuk menguji pengecualian:

@Test public void whenDividerIsZero_thenDivideByZeroExceptionIsThrown() { Calculator calculator = new Calculator(); assertThrows(DivideByZeroException.class, () -> calculator.divide(10, 0)); }

Di JUnit 4, kita dapat mencapainya dengan menggunakan @Test (dijangka = DivideByZeroException.class) melalui API ujian.

Dan dengan TestNG, kami juga dapat melaksanakan perkara yang sama:

@Test(expectedExceptions = ArithmeticException.class) public void givenNumber_whenThrowsException_thenCorrect() { int i = 1 / 0; }

Ciri ini menunjukkan pengecualian apa yang dilemparkan dari sekeping kod, itu adalah sebahagian daripada ujian.

6. Ujian Parameter

Ujian unit berparameter berguna untuk menguji kod yang sama dalam beberapa keadaan. Dengan bantuan ujian unit parameter, kita dapat menetapkan kaedah ujian yang memperoleh data dari beberapa sumber data. Idea utama adalah menjadikan kaedah ujian unit dapat digunakan semula dan diuji dengan satu set input yang berbeza.

Di JUnit 5 , kami mempunyai kelebihan kaedah ujian menggunakan argumen data secara langsung dari sumber yang dikonfigurasi. Secara lalai, JUnit 5 menyediakan beberapa anotasi sumber seperti:

  • @ValueSource: kita dapat menggunakannya dengan pelbagai nilai jenis Short, Byte, Int, Long, Float, Double, Char, dan String:
@ParameterizedTest @ValueSource(strings = { "Hello", "World" }) void givenString_TestNullOrNot(String word) { assertNotNull(word); }
  • @EnumSource - meneruskan pemalar Enum sebagai parameter kepada kaedah ujian:
@ParameterizedTest @EnumSource(value = PizzaDeliveryStrategy.class, names = {"EXPRESS", "NORMAL"}) void givenEnum_TestContainsOrNot(PizzaDeliveryStrategy timeUnit) { assertTrue(EnumSet.of(PizzaDeliveryStrategy.EXPRESS, PizzaDeliveryStrategy.NORMAL).contains(timeUnit)); }
  • @MethodSource - p menilai kaedah luaran menghasilkan aliran:
static Stream wordDataProvider() { return Stream.of("foo", "bar"); } @ParameterizedTest @MethodSource("wordDataProvider") void givenMethodSource_TestInputStream(String argument) { assertNotNull(argument); }
  • @CsvSource - menggunakan nilai CSV sebagai sumber untuk parameter:
@ParameterizedTest @CsvSource({ "1, Car", "2, House", "3, Train" }) void givenCSVSource_TestContent(int id, String word) { assertNotNull(id); assertNotNull(word); }

Begitu juga, kita mempunyai sumber lain seperti @CsvFileSource jika kita perlu membaca fail CSV dari classpath dan @ArgumentSource untuk menentukan ArgumentsProvider yang disesuaikan dan boleh digunakan semula .

In JUnit 4, the test class has to be annotated with @RunWith to make it a parameterized class and @Parameter to use the denote the parameter values for unit test.

In TestNG, we can parametrize tests using @Parameter or @DataProvider annotations. While using the XML file annotate the test method with @Parameter:

@Test @Parameters({"value", "isEven"}) public void givenNumberFromXML_ifEvenCheckOK_thenCorrect(int value, boolean isEven) { Assert.assertEquals(isEven, value % 2 == 0); }

and provide the data in the XML file:

While using information in the XML file is simple and useful, in some cases, you might need to provide more complex data.

For this, we can use the @DataProvider annotation which allows us to map complex parameter types for testing methods.

Here's an example of using @DataProvider for primitive data types:

@DataProvider(name = "numbers") public static Object[][] evenNumbers() { return new Object[][]{{1, false}, {2, true}, {4, true}}; } @Test(dataProvider = "numbers") public void givenNumberFromDataProvider_ifEvenCheckOK_thenCorrect (Integer number, boolean expected) { Assert.assertEquals(expected, number % 2 == 0); }

And @DataProvider for objects:

@Test(dataProvider = "numbersObject") public void givenNumberObjectFromDataProvider_ifEvenCheckOK_thenCorrect (EvenNumber number) { Assert.assertEquals(number.isEven(), number.getValue() % 2 == 0); } @DataProvider(name = "numbersObject") public Object[][] parameterProvider() { return new Object[][]{{new EvenNumber(1, false)}, {new EvenNumber(2, true)}, {new EvenNumber(4, true)}}; }

In the same way, any particular objects that are to be tested can be created and returned using data provider. It's useful when integrating with frameworks like Spring.

Notice that, in TestNG, since @DataProvider method need not be static, we can use multiple data provider methods in the same test class.

7. Test Timeout

Timed out tests means, a test case should fail if the execution is not completed within a certain specified period. Both JUnit and TestNG support timed out tests. In JUnit 5 we can write a timeout test as:

@Test public void givenExecution_takeMoreTime_thenFail() throws InterruptedException { Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(10000)); }

In JUnit 4 and TestNG we can the same test using @Test(timeout=1000)

@Test(timeOut = 1000) public void givenExecution_takeMoreTime_thenFail() { while (true); }

8. Dependent Tests

TestNG supports dependency testing. This means in a set of test methods, if the initial test fails, then all subsequent dependent tests will be skipped, not marked as failed as in the case for JUnit.

Let's have a look at a scenario, where we need to validate email, and if it's successful, will proceed to log in:

@Test public void givenEmail_ifValid_thenTrue() { boolean valid = email.contains("@"); Assert.assertEquals(valid, true); } @Test(dependsOnMethods = {"givenEmail_ifValid_thenTrue"}) public void givenValidEmail_whenLoggedIn_thenTrue() { LOGGER.info("Email {} valid >> logging in", email); }

9. Order of Test Execution

There is no defined implicit order in which test methods will get executed in JUnit 4 or TestNG. The methods are just invoked as returned by the Java Reflection API. Since JUnit 4 it uses a more deterministic but not predictable order.

To have more control, we will annotate the test class with @FixMethodOrder annotation and mention a method sorter:

@FixMethodOrder(MethodSorters.NAME_ASCENDING) public class SortedTests { @Test public void a_givenString_whenChangedtoInt_thenTrue() { assertTrue( Integer.valueOf("10") instanceof Integer); } @Test public void b_givenInt_whenChangedtoString_thenTrue() { assertTrue( String.valueOf(10) instanceof String); } }

The MethodSorters.NAME_ASCENDING parameter sorts the methods by method name is lexicographic order. Apart from this sorter, we have MethodSorter.DEFAULT and MethodSorter.JVM as well.

While TestNG also provides a couple of ways to have control in the order of test method execution. We provide the priority parameter in the @Test annotation:

@Test(priority = 1) public void givenString_whenChangedToInt_thenCorrect() { Assert.assertTrue( Integer.valueOf("10") instanceof Integer); } @Test(priority = 2) public void givenInt_whenChangedToString_thenCorrect() { Assert.assertTrue( String.valueOf(23) instanceof String); }

Notice, that priority invokes test methods based on priority but does not guarantee that tests in one level are completed before invoking the next priority level.

Sometimes while writing functional test cases in TestNG, we might have an interdependent test where the order of execution must be the same for every test run. To achieve that we should use the dependsOnMethods parameter to @Test annotation as we saw in the earlier section.

10. Custom Test Name

By default, whenever we run a test, the test class and the test method name is printed in console or IDE. JUnit 5 provides a unique feature where we can mention custom descriptive names for class and test methods using @DisplayName annotation.

This annotation doesn't provide any testing benefits but it brings easy to read and understand test results for a non-technical person too:

@ParameterizedTest @ValueSource(strings = { "Hello", "World" }) @DisplayName("Test Method to check that the inputs are not nullable") void givenString_TestNullOrNot(String word) { assertNotNull(word); }

Setiap kali kita menjalankan ujian, output akan menunjukkan nama paparan dan bukannya nama kaedah.

Buat masa ini, di TestNG tidak ada cara untuk memberikan nama khusus.

11. Kesimpulannya

Kedua-dua JUnit dan TestNG adalah alat moden untuk pengujian di ekosistem Jawa.

Dalam artikel ini, kami melihat dengan cepat pelbagai cara menulis ujian dengan masing-masing dua kerangka ujian ini.

Pelaksanaan semua potongan kode boleh didapati di projek TestNG dan junit-5 Github.