Tutorial JUnit 5, bagian 1: Pengujian unit dengan JUnit 5, Mockito, dan Hamcrest

JUnit 5 adalah standar de facto baru untuk mengembangkan pengujian unit di Java. Versi terbaru ini telah meninggalkan batasan Java 5 dan mengintegrasikan banyak fitur dari Java 8, terutama dukungan untuk ekspresi lambda.

Di paruh pertama dari pengantar dua bagian JUnit 5 ini, Anda akan memulai pengujian dengan JUnit 5. Saya akan menunjukkan kepada Anda cara mengkonfigurasi proyek Maven untuk menggunakan JUnit 5, cara menulis pengujian menggunakan anotasi @Testdan @ParameterizedTest, dan cara bekerja dengan anotasi siklus hidup baru di JUnit 5. Anda juga akan melihat contoh singkat penggunaan tag filter, dan saya akan menunjukkan cara mengintegrasikan JUnit 5 dengan pustaka pernyataan pihak ketiga — dalam hal ini, Hamcrest . Terakhir, Anda akan mendapatkan pengantar tutorial singkat untuk mengintegrasikan JUnit 5 dengan Mockito, sehingga Anda dapat menulis pengujian unit yang lebih kuat untuk sistem dunia nyata yang kompleks.

download Dapatkan kode Dapatkan kode sumbernya sebagai contoh dalam tutorial ini. Dibuat oleh Steven Haines untuk JavaWorld.

Pengembangan yang digerakkan oleh pengujian

Jika Anda telah mengembangkan kode Java untuk jangka waktu tertentu, Anda mungkin sangat akrab dengan pengembangan berbasis pengujian, jadi saya akan membuat bagian ini singkat. Namun, penting untuk memahami mengapa kami menulis pengujian unit, serta strategi yang digunakan developer saat merancang pengujian unit.

Test-driven development (TDD) adalah proses pengembangan perangkat lunak yang menjalin pengkodean, pengujian, dan desain. Ini adalah pendekatan uji coba yang bertujuan untuk meningkatkan kualitas aplikasi Anda. Pengembangan berbasis pengujian ditentukan oleh siklus proses berikut:

  1. Tambahkan tes.
  2. Jalankan semua pengujian Anda dan amati kegagalan pengujian baru.
  3. Terapkan kodenya.
  4. Jalankan semua pengujian Anda dan amati pengujian baru yang berhasil.
  5. Refactor kode tersebut.

Gambar 1 menunjukkan siklus hidup TDD ini.

Steven Haines

Ada dua tujuan untuk menulis tes sebelum menulis kode Anda. Pertama, ini memaksa Anda untuk memikirkan masalah bisnis yang Anda coba selesaikan. Misalnya, bagaimana skenario yang berhasil harus berperilaku? Kondisi apa yang harus gagal? Bagaimana mereka harus gagal? Kedua, pengujian pertama memberi Anda lebih percaya diri dalam pengujian Anda. Setiap kali saya menulis tes setelah menulis kode, saya selalu harus memecahkannya untuk memastikan bahwa mereka benar-benar menangkap kesalahan. Menulis tes terlebih dahulu menghindari langkah ekstra ini.

Menulis pengujian untuk jalur bahagia biasanya mudah: Dengan masukan yang baik, kelas harus mengembalikan respons deterministik. Tetapi menulis kasus uji negatif (atau kegagalan), terutama untuk komponen yang kompleks, bisa jadi lebih rumit.

Sebagai contoh, pertimbangkan untuk menulis tes untuk repositori database. Di jalur bahagia, kami memasukkan catatan ke dalam database dan menerima kembali objek yang dibuat, termasuk kunci yang dihasilkan. Pada kenyataannya, kita juga harus mempertimbangkan kemungkinan konflik, seperti memasukkan record dengan nilai kolom unik yang sudah dipegang oleh record lain. Selain itu, apa yang terjadi jika repositori tidak dapat terhubung ke database, mungkin karena nama pengguna atau kata sandi telah berubah? Apa yang terjadi jika ada kesalahan jaringan saat transit? Apa yang terjadi jika permintaan tidak selesai dalam batas waktu tunggu yang Anda tentukan?

Untuk membangun komponen yang kuat, Anda perlu mempertimbangkan semua skenario yang mungkin dan tidak mungkin, mengembangkan pengujian untuk mereka, dan menulis kode Anda untuk memenuhi pengujian tersebut. Nanti di artikel ini, kita akan melihat strategi untuk membuat skenario kegagalan yang berbeda, bersama dengan beberapa fitur baru di JUnit 5 yang dapat membantu Anda menguji skenario tersebut.

Mengadopsi JUnit 5

Jika Anda telah menggunakan JUnit selama beberapa waktu, beberapa perubahan di JUnit 5 akan menjadi penyesuaian. Berikut ringkasan tingkat tinggi tentang apa yang berbeda antara kedua versi:

  • JUnit 5 sekarang dikemas dalam org.junit.jupitergrup, yang mengubah cara Anda memasukkannya ke dalam proyek Maven dan Gradle Anda.
  • JUnit 4 membutuhkan JDK minimal JDK 5; JUnit 5 membutuhkan minimal JDK 8.
  • JUnit 4 ini @Before, @BeforeClass, @After, dan @AfterClasspenjelasan telah digantikan oleh @BeforeEach, @BeforeAll, @AfterEach, dan @AfterAllmasing-masing.
  • @IgnoreAnotasi JUnit 4 telah diganti dengan @Disabledanotasi.
  • The @Categoryanotasi telah digantikan oleh @Tagpenjelasan.
  • JUnit 5 menambahkan satu set metode pernyataan baru.
  • Runner telah diganti dengan ekstensi, dengan API baru untuk pelaksana ekstensi.
  • JUnit 5 memperkenalkan asumsi yang menghentikan eksekusi pengujian.
  • JUnit 5 mendukung kelas pengujian bersarang dan dinamis.

Kami akan menjelajahi sebagian besar fitur baru ini di artikel ini.

Pengujian unit dengan JUnit 5

Mari kita mulai dengan sederhana, dengan contoh end-to-end mengkonfigurasi proyek untuk menggunakan JUnit 5 untuk pengujian unit. Kode 1 menunjukkan MathToolskelas yang metodenya mengubah pembilang dan penyebut menjadi a double.

Kode 1. Contoh proyek JUnit 5 (MathTools.java)

 package com.javaworld.geekcap.math; public class MathTools { public static double convertToDecimal(int numerator, int denominator) { if (denominator == 0) { throw new IllegalArgumentException("Denominator must not be 0"); } return (double)numerator / (double)denominator; } }

Kami memiliki dua skenario utama untuk menguji MathToolskelas dan metodenya:

  • Sebuah tes yang valid , di mana kami melewati bilangan bulat non-nol untuk pembilang dan penyebut.
  • Sebuah skenario kegagalan , di mana kami melewati nilai nol untuk penyebut.

Kode 2 menunjukkan kelas uji JUnit 5 untuk menguji dua skenario ini.

Kode 2. Kelas uji JUnit 5 (MathToolsTest.java)

 package com.javaworld.geekcap.math; import java.lang.IllegalArgumentException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; class MathToolsTest { @Test void testConvertToDecimalSuccess() { double result = MathTools.convertToDecimal(3, 4); Assertions.assertEquals(0.75, result); } @Test void testConvertToDecimalInvalidDenominator() { Assertions.assertThrows(IllegalArgumentException.class, () -> MathTools.convertToDecimal(3, 0)); } }

In Listing 2, the testConvertToDecimalInvalidDenominator method executes the MathTools::convertToDecimal method inside an assertThrows call. The first argument is the expected type of exception to be thrown. The second argument is a function that will throw that exception. The assertThrows method executes the function and validates that the expected type of exception is thrown.

The Assertions class and its methods

The org.junit.jupiter.api.Test annotation denotes a test method. Note that the @Test annotation now comes from the JUnit 5 Jupiter API package instead of JUnit 4's org.junit package. The testConvertToDecimalSuccess method first executes the MathTools::convertToDecimal method with a numerator of 3 and a denominator of 4, then asserts that the result is equal to 0.75. The org.junit.jupiter.api.Assertions class provides a set of static methods for comparing actual and expected results. The Assertions class has the following methods, which cover most of the primitive data types:

  • assertArrayEquals compares the contents of an actual array to an expected array.
  • assertEquals compares an actual value to an expected value.
  • assertNotEquals compares two values to validate that they are not equal.
  • assertTrue validates that the provided value is true.
  • assertFalse validates that the provided value is false.
  • assertLinesMatch compares two lists of Strings.
  • assertNull validates that the provided value is null.
  • assertNotNull validates that the provided value is not null.
  • assertSame validates that two values reference the same object.
  • assertNotSame validates that two values do not reference the same object.
  • assertThrows validates that the execution of a method throws an expected exception (you can see this in the testConvertToDecimalInvalidDenominator example above).
  • assertTimeout validates that a supplied function completes within a specified timeout.
  • assertTimeoutPreemptively validates that a supplied function completes within a specified timeout, but once the timeout is reached it kills the function's execution.

If any of these assertion methods fail, the unit test is marked as failed. That failure notice will be written to the screen when you run the test, then saved in a report file.

Using delta with assertEquals

When using float and double values in an assertEquals, you can also specify a delta that represents a threshold of difference between the two. In our example we could have added a delta of 0.001, in case 0.75 was actually returned as 0.750001.

Analyzing your test results

In addition to validating a value or behavior, the assert methods can also accept a textual description of the error, which can help you diagnose failures. For example:

 Assertions.assertEquals(0.75, result, "The MathTools::convertToDecimal value did not return the correct value of 0.75 for 3/4"); Assertions.assertEquals(0.75, result, () -> "The MathTools::convertToDecimal value did not return the correct value of 0.75 for 3/4"); 

The output will show the expected value of 0.75 and the actual value. It will also display the specified message, which can help you understand the context of the error. The difference between the two variations is that the first one always creates the message, even if it is not displayed, whereas the second one only constructs the message if the assertion fails. In this case, the construction of the message is trivial, so it doesn't really matter. Still, there is no need to construct an error message for a test that passes, so it's usually a best practice to use the second style.

Finally, if you're using an IDE like IntelliJ to run your tests, each test method will be displayed by its method name. This is fine if your method names are readable, but you can also add a @DisplayName annotation to your test methods to better identify the tests:

@Test @DisplayName("Test successful decimal conversion") void testConvertToDecimalSuccess() { double result = MathTools.convertToDecimal(3, 4); Assertions.assertEquals(0.751, result); }

Running your unit test

In order to run JUnit 5 tests from a Maven project, you need to include the maven-surefire-plugin in the Maven pom.xml file and add a new dependency. Listing 3 shows the pom.xml file for this project.

Listing 3. Maven pom.xml for an example JUnit 5 project

  4.0.0 com.javaworld.geekcap junit5 jar 1.0-SNAPSHOT    org.apache.maven.plugins maven-compiler-plugin 3.8.1  8 8    org.apache.maven.plugins maven-surefire-plugin 3.0.0-M4    junit5 //maven.apache.org   org.junit.jupiter junit-jupiter 5.6.0 test   

JUnit 5 dependencies

JUnit 5 packages its components in the org.junit.jupiter group and we need to add the junit-jupiter artifact, which is an aggregator artifact that imports the following dependencies:

  • junit-jupiter-api defines the API for writing tests and extensions.
  • junit-jupiter-engine adalah implementasi mesin uji yang menjalankan pengujian unit.
  • junit-jupiter-params memberikan dukungan untuk pengujian berparameter.

Selanjutnya, kita perlu menambahkan maven-surefire-pluginplug-in build untuk menjalankan pengujian.

Terakhir, pastikan untuk menyertakan maven-compiler-plugindengan versi Java 8 atau yang lebih baru, sehingga Anda akan dapat menggunakan fitur Java 8 seperti lambda.

Menjalankannya!

Gunakan perintah berikut untuk menjalankan kelas pengujian dari IDE Anda atau dari Maven:

mvn clean test

Jika Anda berhasil, Anda akan melihat keluaran yang mirip dengan berikut ini:

 [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.javaworld.geekcap.math.MathToolsTest [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.04 s - in com.javaworld.geekcap.math.MathToolsTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 3.832 s [INFO] Finished at: 2020-02-16T08:21:15-05:00 [INFO] ------------------------------------------------------------------------