Tutorial JUnit 5, bahagian 1: Uji unit dengan JUnit 5, Mockito, dan Hamcrest

JUnit 5 adalah standard de facto baru untuk mengembangkan ujian unit di Java. Versi terbaru ini telah meninggalkan batasan Java 5 dan menyatukan banyak ciri dari Java 8, terutamanya sokongan untuk ungkapan lambda.

Pada separuh pertama pengenalan dua bahagian ini kepada JUnit 5, anda akan memulakan ujian dengan JUnit 5. Saya akan menunjukkan kepada anda cara mengkonfigurasi projek Maven untuk menggunakan JUnit 5, cara menulis ujian menggunakan @Testdan @ParameterizedTestanotasi, dan cara bekerja dengan anotasi kitaran hidup yang baru di JUnit 5. Anda juga akan melihat contoh ringkas penggunaan tag penapis, dan saya akan menunjukkan kepada anda cara mengintegrasikan JUnit 5 dengan perpustakaan penegasan pihak ketiga — dalam kes ini, Hamcrest . Akhirnya, anda akan mendapat pengenalan tutorial yang cepat untuk mengintegrasikan JUnit 5 dengan Mockito, supaya anda dapat menulis ujian unit yang lebih mantap untuk sistem dunia nyata yang kompleks.

muat turun Dapatkan kod Dapatkan kod sumber untuk contoh dalam tutorial ini. Dicipta oleh Steven Haines untuk JavaWorld.

Pembangunan berasaskan ujian

Sekiranya anda telah mengembangkan kod Java untuk jangka masa tertentu, anda mungkin sangat akrab dengan pengembangan berdasarkan ujian, jadi saya akan memaklumkan bahagian ini ringkas. Penting untuk memahami mengapa kita menulis ujian unit, begitu juga strategi yang digunakan pembangun semasa merancang ujian unit.

Pengembangan berasaskan ujian (TDD) adalah proses pengembangan perisian yang merangkumi pengekodan, pengujian, dan reka bentuk. Ini adalah pendekatan pertama yang bertujuan untuk meningkatkan kualiti aplikasi anda. Perkembangan berdasarkan ujian ditentukan oleh kitaran hidup berikut:

  1. Tambah ujian.
  2. Jalankan semua ujian anda dan perhatikan ujian baru gagal.
  3. Laksanakan kod.
  4. Jalankan semua ujian anda dan perhatikan ujian baru yang berjaya.
  5. Refaktor kod.

Rajah 1 menunjukkan kitaran hidup TDD ini.

Steven Haines

Terdapat dua tujuan untuk menulis ujian sebelum menulis kod anda. Pertama, ini memaksa anda untuk memikirkan masalah perniagaan yang ingin anda selesaikan. Sebagai contoh, bagaimana senario yang berjaya boleh berlaku? Keadaan apa yang mesti gagal? Bagaimana mereka harus gagal? Kedua, ujian terlebih dahulu memberi anda lebih yakin pada ujian anda. Setiap kali saya menulis ujian selepas menulis kod, saya selalu harus memecahkannya untuk memastikan bahawa mereka benar-benar menangkap kesalahan. Ujian menulis terlebih dahulu mengelakkan langkah tambahan ini.

Ujian menulis untuk jalan senang biasanya mudah: Dengan memberikan input yang baik, kelas harus memberikan respons yang pasti. Tetapi menulis kes ujian negatif (atau kegagalan), terutama untuk komponen kompleks, boleh menjadi lebih rumit.

Sebagai contoh, pertimbangkan untuk menulis ujian untuk repositori pangkalan data. Di jalan senang, kami memasukkan rekod ke dalam pangkalan data dan menerima kembali objek yang dibuat, termasuk kunci yang dihasilkan. Pada hakikatnya, kita juga harus mempertimbangkan kemungkinan konflik, seperti memasukkan rekod dengan nilai lajur unik yang sudah dipegang oleh rekod lain. Selain itu, apa yang berlaku apabila repositori tidak dapat menyambung ke pangkalan data, mungkin kerana nama pengguna atau kata laluan telah berubah? Apa yang berlaku sekiranya terdapat ralat dalam perjalanan? Apa yang berlaku jika permintaan tidak selesai dalam had masa tamat yang anda tentukan?

Untuk membina komponen yang kuat, anda perlu mempertimbangkan semua senario yang mungkin dan tidak mungkin, buat ujian untuknya, dan tulis kod anda untuk memenuhi ujian tersebut. Kemudian dalam artikel, kita akan melihat strategi untuk membuat senario kegagalan yang berbeza, bersama dengan beberapa ciri baru di JUnit 5 yang dapat membantu anda menguji senario tersebut.

Mengamalkan JUnit 5

Sekiranya anda telah menggunakan JUnit untuk sementara waktu, beberapa perubahan dalam JUnit 5 akan menjadi penyesuaian. Berikut adalah ringkasan tahap tinggi mengenai perbezaan antara dua versi:

  • JUnit 5 kini dikemas dalam org.junit.jupiterkumpulan, yang mengubah cara anda memasukkannya ke dalam projek Maven dan Gradle anda.
  • JUnit 4 memerlukan JDK minimum JDK 5; JUnit 5 memerlukan minimum JDK 8.
  • Junit 4 ini @Before, @BeforeClass, @After, dan @AfterClasspenjelasan telah digantikan dengan @BeforeEach, @BeforeAll, @AfterEach, dan @AfterAllmasing-masing.
  • Anotasi JUnit 4 @Ignoretelah digantikan dengan @Disabledanotasi.
  • The @Categoryanotasi telah digantikan dengan @Taganotasi.
  • JUnit 5 menambah satu set kaedah penegasan baru.
  • Pelari telah diganti dengan pelanjutan, dengan API baru untuk pelaksana peluasan.
  • JUnit 5 memperkenalkan andaian yang menghentikan ujian daripada dilaksanakan.
  • JUnit 5 menyokong kelas ujian bersarang dan dinamik.

Kami akan meneroka sebahagian besar ciri baru ini dalam artikel ini.

Ujian unit dengan JUnit 5

Mari kita mulakan dengan mudah, dengan contoh konfigurasi projek dari hujung ke hujung untuk menggunakan JUnit 5 untuk ujian unit. Penyenaraian 1 menunjukkan MathToolskelas yang kaedahnya menukar pembilang dan penyebut menjadi a double.

Penyenaraian 1. Contoh projek 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 mempunyai dua senario utama untuk menguji MathToolskelas dan kaedahnya:

  • A ujian sah , di mana kita lulus integer bukan sifar untuk pengangka dan penyebut.
  • A senario kegagalan , di mana kita lulus nilai sifar untuk penyebut.

Penyenaraian 2 menunjukkan kelas ujian JUnit 5 untuk menguji kedua-dua senario ini.

Penyenaraian 2. Kelas ujian 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)); } }

Dalam Penyenaraian 2, testConvertToDecimalInvalidDenominatorkaedah melaksanakan MathTools::convertToDecimalkaedah dalam assertThrowspanggilan. Hujah pertama adalah jenis pengecualian yang diharapkan untuk dilemparkan. Hujah kedua adalah fungsi yang akan membuang pengecualian itu. The assertThrowsCara melaksanakan fungsi dan mengesahkan bahawa jenis yang diharapkan daripada pengecualian dibuang.

Kelas Assertions dan kaedahnya

The  org.junit.jupiter.api.Testanotasi menandakan kaedah ujian. Perhatikan bahawa @Testanotasi kini berasal dari pakej JUnit 5 Jupiter API dan bukannya pakej JUnit 4 org.junit. The testConvertToDecimalSuccessKaedah pertama melaksanakan yang MathTools::convertToDecimalkaedah dengan pengangka 3 dan penyebut 4, kemudian menegaskan bahawa keputusan itu adalah sama dengan 0.75. The org.junit.jupiter.api.Assertionskelas menyediakan satu set statickaedah untuk membandingkan keputusan sebenar dan dijangka. The Assertionskelas mempunyai kaedah berikut, yang meliputi sebahagian besar jenis data primitif:

  • assertArrayEquals membandingkan kandungan array sebenar dengan array yang diharapkan.
  • assertEquals membandingkan nilai sebenar dengan nilai yang diharapkan.
  • assertNotEquals membandingkan dua nilai untuk mengesahkan bahawa mereka tidak sama.
  • assertTrue mengesahkan bahawa nilai yang diberikan adalah benar.
  • assertFalse mengesahkan bahawa nilai yang diberikan adalah salah.
  • assertLinesMatchmembandingkan dua senarai 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 pelaksanaan mesin ujian yang menjalankan ujian unit.
  • junit-jupiter-params memberikan sokongan untuk ujian parameter.

Seterusnya, kita perlu menambahkan maven-surefire-pluginplug-in build untuk menjalankan ujian.

Akhirnya, pastikan untuk menyertakan maven-compiler-plugindengan versi Java 8 atau lebih baru, sehingga anda dapat menggunakan fitur Java 8 seperti lambdas.

Jalankannya!

Gunakan arahan berikut untuk menjalankan kelas ujian dari IDE anda atau dari Maven:

mvn clean test

Sekiranya anda berjaya, anda akan melihat output yang serupa dengan yang berikut:

 [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] ------------------------------------------------------------------------