Panduan untuk Pengujian Unit dan Integrasi yang Kuat dengan JUnit
Diterbitkan: 2022-03-11Pengujian perangkat lunak otomatis sangat penting untuk kualitas jangka panjang, pemeliharaan, dan ekstensibilitas proyek perangkat lunak, dan untuk Java, JUnit adalah jalan menuju otomatisasi.
Sementara sebagian besar artikel ini akan fokus pada penulisan pengujian unit yang kuat dan penggunaan stubbing, mocking, dan injeksi ketergantungan, kami juga akan membahas JUnit dan pengujian integrasi.
Kerangka uji JUnit adalah alat umum, gratis, dan sumber terbuka untuk menguji proyek berbasis Java.
Pada tulisan ini, JUnit 4 adalah rilis utama saat ini, telah dirilis lebih dari 10 tahun yang lalu, dengan pembaruan terakhir lebih dari dua tahun yang lalu.
JUnit 5 (dengan pemrograman Jupiter dan model ekstensi) sedang dalam pengembangan aktif. Ini lebih mendukung fitur bahasa yang diperkenalkan di Java 8 dan menyertakan fitur baru dan menarik lainnya. Beberapa tim mungkin menemukan JUnit 5 siap digunakan, sementara yang lain mungkin terus menggunakan JUnit 4 hingga 5 dirilis secara resmi. Kita akan melihat contoh dari keduanya.
Menjalankan JUnit
Tes JUnit dapat dijalankan langsung di IntelliJ, tetapi juga dapat dijalankan di IDE lain seperti Eclipse, NetBeans, atau bahkan baris perintah.
Pengujian harus selalu berjalan pada waktu pembuatan, terutama pengujian unit. Sebuah build dengan pengujian yang gagal harus dianggap gagal, terlepas dari apakah masalahnya ada pada produksi atau kode pengujian– ini membutuhkan disiplin dari tim dan kemauan untuk memberikan prioritas tertinggi untuk menyelesaikan pengujian yang gagal, tetapi perlu untuk mematuhi semangat otomatisasi.
Tes JUnit juga dapat dijalankan dan dilaporkan oleh sistem integrasi berkelanjutan seperti Jenkins. Proyek yang menggunakan alat seperti Gradle, Maven, atau Ant memiliki keuntungan tambahan karena dapat menjalankan pengujian sebagai bagian dari proses pembangunan.
Gradle
Sebagai contoh proyek Gradle untuk JUnit 5, lihat bagian Gradle dari panduan pengguna JUnit dan repositori junit5-samples.git. Perhatikan bahwa itu juga dapat menjalankan pengujian yang menggunakan JUnit 4 API (disebut sebagai “vintage” ).
Proyek dapat dibuat di IntelliJ melalui opsi menu File > Open… > navigasikan ke junit-gradle-consumer sub-directory
> OK > Open as Project > OK untuk mengimpor proyek dari Gradle.
Untuk Eclipse, plugin Buildship Gradle dapat diinstal dari Help > Eclipse Marketplace… Proyek kemudian dapat diimpor dengan File > Import… > Gradle > Gradle Project > Next > Next > Browse to the junit-gradle-consumer
sub-directory > Next > Berikutnya > Selesai.
Setelah menyiapkan proyek Gradle di IntelliJ atau Eclipse, menjalankan tugas build
Gradle akan mencakup menjalankan semua pengujian JUnit dengan tugas test
. Perhatikan bahwa pengujian mungkin dilewati pada eksekusi build
berikutnya jika tidak ada perubahan yang dilakukan pada kode.
Untuk JUnit 4, lihat penggunaan JUnit dengan wiki Gradle.
Maven
Untuk JUnit 5, lihat bagian Maven dari panduan pengguna dan repositori junit5-samples.git untuk contoh proyek Maven. Ini juga dapat menjalankan pengujian vintage (yang menggunakan JUnit 4 API).
Di IntelliJ, gunakan File > Open… > navigasikan ke junit-maven-consumer/pom.xml
> OK > Open as Project. Tes kemudian dapat dijalankan dari Maven Projects > junit5-maven-consumer > Lifecycle > Test.
Di Eclipse, gunakan File > Import… > Maven > Existing Maven Projects > Next > Browse to the junit-maven-consumer
directory > Dengan pom.xml
yang dipilih > Finish.
Tes dapat dijalankan dengan menjalankan proyek sebagai Maven build… > tentukan tujuan test
> Jalankan.
Untuk JUnit 4, lihat JUnit di repositori Maven.
Lingkungan Pengembangan
Selain menjalankan pengujian melalui alat build seperti Gradle atau Maven, banyak IDE dapat langsung menjalankan pengujian JUnit.
IntelliJ IDEA
IntelliJ IDEA 2016.2 atau yang lebih baru diperlukan untuk pengujian JUnit 5, sedangkan pengujian JUnit 4 harus bekerja di versi IntelliJ yang lebih lama.
Untuk tujuan artikel ini, Anda mungkin ingin membuat proyek baru di IntelliJ dari salah satu repositori GitHub saya ( JUnit5IntelliJ.git atau JUnit4IntelliJ.git), yang menyertakan semua file dalam contoh kelas Person
sederhana dan menggunakan built-in perpustakaan JUnit. Tes dapat dijalankan dengan Run > Run 'All Tests'. Tes juga dapat dijalankan di IntelliJ dari kelas PersonTest
.
Repositori ini dibuat dengan proyek IntelliJ Java baru dan membangun struktur direktori src/main/java/com/example
dan src/test/java/com/example
. Direktori src/main/java
ditetapkan sebagai folder sumber sementara src/test/java
ditetapkan sebagai folder sumber uji. Setelah membuat kelas PersonTest
dengan metode pengujian yang dijelaskan dengan @Test
, kelas tersebut mungkin gagal dikompilasi, dalam hal ini IntelliJ menawarkan saran untuk menambahkan JUnit 4 atau JUnit 5 ke jalur kelas yang dapat dimuat dari distribusi IntelliJ IDEA (lihat ini jawaban di Stack Overflow untuk lebih jelasnya). Terakhir, konfigurasi run JUnit ditambahkan untuk Semua Pengujian.
Lihat juga Panduan Cara Menguji IntelliJ.
Gerhana
Proyek Java kosong di Eclipse tidak akan memiliki direktori root pengujian. Ini telah ditambahkan dari Project Properties > Java Build Path > Add Folder… > Create New Folder… > tentukan nama Folder > Finish. Direktori baru akan dipilih sebagai folder sumber. Klik OK di kedua dialog yang tersisa.
Tes JUnit 4 dapat dibuat dengan File > New > JUnit Test Case. Pilih "Tes JUnit 4 baru" dan folder sumber yang baru dibuat untuk pengujian. Tentukan "kelas yang sedang diuji" dan "paket", pastikan paket tersebut cocok dengan kelas yang sedang diuji. Kemudian, tentukan nama untuk kelas pengujian. Setelah menyelesaikan wizard, jika diminta, pilih "Add JUnit 4 library" ke jalur build. Proyek atau kelas uji individu kemudian dapat dijalankan sebagai Uji JUnit. Lihat juga Pengujian Eclipse Menulis dan Menjalankan JUnit.
NetBeans
NetBeans hanya mendukung pengujian JUnit 4. Kelas pengujian dapat dibuat dalam proyek Java NetBeans dengan File > File Baru… > Unit Tests > JUnit Test atau Test for Existing Class. Secara default, direktori root uji diberi nama test
di direktori proyek.
Kelas Produksi Sederhana dan Kasus Uji JUnitnya
Mari kita lihat contoh sederhana dari kode produksi dan kode unit test yang sesuai untuk kelas Person
yang sangat sederhana. Anda dapat mengunduh kode sampel dari proyek github saya dan membukanya melalui IntelliJ.
src/main/java/com/contoh/Person.java
package com.example; class Person { private final String givenName; private final String surname; Person(String givenName, String surname) { this.givenName = givenName; this.surname = surname; } String getDisplayName() { return surname + ", " + givenName; } }
Kelas Person
yang tidak dapat diubah memiliki konstruktor dan metode getDisplayName()
. Kami ingin menguji bahwa getDisplayName()
mengembalikan nama yang diformat seperti yang kami harapkan. Berikut adalah kode pengujian untuk pengujian unit tunggal (JUnit 5):
src/test/java/com/example/PersonTest.java
package com.example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class PersonTest { @Test void testGetDisplayName() { Person person = new Person("Josh", "Hayden"); String displayName = person.getDisplayName(); assertEquals("Hayden, Josh", displayName); } }
PersonTest
menggunakan @Test
dan pernyataan JUnit 5. Untuk JUnit 4, kelas dan metode PersonTest
harus publik dan impor yang berbeda harus digunakan. Inilah contoh JUnit 4 Intisari.
Saat menjalankan kelas PersonTest
di IntelliJ, tes lulus dan indikator UI berwarna hijau.
Konvensi JUnit Umum
Penamaan
Meskipun tidak diperlukan, kami menggunakan konvensi umum dalam penamaan kelas tes; khusus, kita mulai dengan nama kelas yang sedang diuji ( Person
) dan menambahkan "Test" ke dalamnya ( PersonTest
). Penamaan metode pengujian serupa, dimulai dengan metode yang diuji ( getDisplayName()
) dan menambahkan "test" padanya ( testGetDisplayName()
). Meskipun ada banyak konvensi lain yang dapat diterima untuk menamai metode pengujian, penting untuk konsisten di seluruh tim dan proyek.
Nama di Produksi | Nama dalam Pengujian |
---|---|
Orang | Tes Orang |
getDisplayName() | testDisplayName() |
Paket
Kami juga menggunakan konvensi pembuatan kelas PersonTest
kode pengujian dalam paket yang sama ( com.example
) sebagai kelas Person
kode produksi. Jika kami menggunakan paket yang berbeda untuk pengujian, kami akan diminta untuk menggunakan pengubah akses publik di kelas kode produksi, konstruktor, dan metode yang dirujuk oleh pengujian unit, bahkan jika itu tidak sesuai, jadi lebih baik menyimpannya dalam paket yang sama . Namun, kami menggunakan direktori sumber terpisah ( src/main/java
dan src/test/java
) karena kami biasanya tidak ingin menyertakan kode uji dalam build produksi yang dirilis.
Struktur dan Anotasi
Anotasi @Test
(JUnit 4/5) memberi tahu JUnit untuk menjalankan metode testGetDisplayName()
sebagai metode pengujian dan melaporkan apakah metode tersebut lolos atau gagal. Selama semua pernyataan (jika ada) lulus dan tidak ada pengecualian yang dilemparkan, tes dianggap lulus.
Kode pengujian kami mengikuti pola struktur Arrange-Act-Assert (AAA). Pola umum lainnya termasuk Given-When-then dan Setup-Exercise-Verify-Teardown (Teardown biasanya tidak secara eksplisit diperlukan untuk pengujian unit), tetapi kami menggunakan AAA dalam artikel ini.
Mari kita lihat bagaimana contoh pengujian kita mengikuti AAA. Baris pertama, "arrange" membuat objek Person
yang akan diuji:
Person person = new Person("Josh", "Hayden");
Baris kedua, "act," menjalankan metode Person.getDisplayName()
kode produksi:
String displayName = person.getDisplayName();
Baris ketiga, "menegaskan," memverifikasi bahwa hasilnya seperti yang diharapkan.
assertEquals("Hayden, Josh", displayName);
Secara internal, panggilan assertEquals()
menggunakan metode equals objek String “Hayden, Josh” untuk memverifikasi nilai aktual yang dikembalikan dari kecocokan kode produksi ( displayName
). Jika tidak cocok, tes akan ditandai gagal.
Perhatikan bahwa pengujian sering kali memiliki lebih dari satu baris untuk setiap fase AAA ini.
Tes Unit dan Kode Produksi
Sekarang setelah kita membahas beberapa konvensi pengujian, mari kita alihkan perhatian kita untuk membuat kode produksi dapat diuji.
Kami kembali ke kelas Person
kami, di mana saya telah menerapkan metode untuk mengembalikan usia seseorang berdasarkan tanggal lahirnya. Contoh kode memerlukan Java 8 untuk memanfaatkan tanggal baru dan API fungsional. Berikut tampilan kelas Person.java
baru:
orang.java
// ... class Person { // ... private final LocalDate dateOfBirth; Person(String givenName, String surname, LocalDate dateOfBirth) { // ... this.dateOfBirth = dateOfBirth; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, LocalDate.now()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }
Menjalankan kelas ini (pada saat penulisan) mengumumkan bahwa Joey berusia 4 tahun. Mari tambahkan metode pengujian:
PersonTest.java
// ... class PersonTest { // ... @Test void testGetAge() { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); long age = person.getAge(); assertEquals(4, age); } }
Itu berlalu hari ini, tetapi bagaimana ketika kita menjalankannya satu tahun dari sekarang? Pengujian ini bersifat non-deterministik dan rapuh karena hasil yang diharapkan tergantung pada tanggal saat sistem menjalankan pengujian.
Menghentikan dan Menyuntikkan Pemasok Nilai
Saat menjalankan produksi, kami ingin menggunakan tanggal saat ini, LocalDate.now()
, untuk menghitung usia orang tersebut, tetapi untuk membuat tes deterministik bahkan dalam satu tahun dari sekarang, tes perlu menyediakan nilai currentDate
mereka sendiri.
Ini dikenal sebagai injeksi ketergantungan. Kami tidak ingin objek Person
kami menentukan tanggal saat ini, tetapi kami ingin meneruskan logika ini sebagai ketergantungan. Pengujian unit akan menggunakan nilai yang diketahui, di-stub, dan kode produksi akan memungkinkan nilai aktual diberikan oleh sistem saat runtime.
Mari tambahkan pemasok LocalDate
ke Person.java
:
orang.java
// ... class Person { // ... private final LocalDate dateOfBirth; private final Supplier<LocalDate> currentDateSupplier; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier) { // ... this.dateOfBirth = dateOfBirth; this.currentDateSupplier = currentDateSupplier; } // ... long getAge() { return ChronoUnit.YEARS.between(dateOfBirth, currentDateSupplier.get()); } public static void main(String... args) { Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12")); System.out.println(person.getDisplayName() + ": " + person.getAge() + " years"); // Doe, Joey: 4 years } }
Untuk mempermudah pengujian metode getAge()
, kami mengubahnya menjadi menggunakan currentDateSupplier
, pemasok LocalDate
, untuk mengambil tanggal saat ini. Jika Anda tidak tahu apa itu pemasok, saya sarankan membaca tentang Antarmuka Fungsional Bawaan Lambda.
Kami juga menambahkan injeksi ketergantungan: Konstruktor pengujian baru memungkinkan pengujian untuk memasok nilai tanggal mereka sendiri saat ini. Konstruktor asli memanggil konstruktor baru ini, meneruskan referensi metode statis LocalDate::now
, yang memasok objek LocalDate
, jadi metode utama kita masih berfungsi seperti sebelumnya. Bagaimana dengan metode pengujian kami? Mari perbarui PersonTest.java
:
PersonTest.java
// ... class PersonTest { // ... @Test void testGetAge() { LocalDate dateOfBirth = LocalDate.parse("2013-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-17"); Person person = new Person("Joey", "Doe", dateOfBirth, ()->currentDate); long age = person.getAge(); assertEquals(4, age); } }
Pengujian sekarang menyuntikkan nilai currentDate
-nya sendiri, jadi pengujian kami akan tetap lulus saat dijalankan tahun depan, atau selama tahun apa pun. Ini biasanya disebut sebagai stubbing , atau memberikan nilai yang diketahui untuk dikembalikan, tetapi pertama-tama kita harus mengubah Person
agar ketergantungan ini dapat disuntikkan.
Perhatikan sintaks lambda ( ()->currentDate
) saat membuat objek Person
. Ini diperlakukan sebagai pemasok LocalDate
, seperti yang dipersyaratkan oleh konstruktor baru.
Mengejek dan Menghentikan Layanan Web
Kami siap untuk objek Person
kami—yang seluruh keberadaannya telah berada di memori JVM—untuk berkomunikasi dengan dunia luar. Kami ingin menambahkan dua metode: metode publishAge()
, yang akan memposting usia orang tersebut saat ini, dan metode getThoseInCommon()
, yang akan menampilkan nama orang terkenal yang memiliki tanggal lahir yang sama atau seumuran dengan Person
kami. Misalkan ada layanan RESTful yang dapat kita gunakan untuk berinteraksi yang disebut “Orang yang Ulang Tahun.” Kami memiliki klien Java untuk itu yang terdiri dari kelas tunggal, BirthdaysClient
.
com.example.birthdays.BirthdaysClient
package com.example.birthdays; import java.io.IOException; import java.util.Arrays; import java.util.Collection; public class BirthdaysClient { public void publishRegularPersonAge(String name, long age) throws IOException { System.out.println("publishing " + name + "'s age: " + age); // HTTP POST with name and age and possibly throw an exception } public Collection<String> findFamousNamesOfAge(long age) throws IOException { System.out.println("finding famous names of age " + age); return Arrays.asList(/* HTTP GET with age and possibly throw an exception */); } public Collection<String> findFamousNamesBornOn(int month, int dayOfMonth) throws IOException { System.out.println("finding famous names born on day " + dayOfMonth + " of month " + month); return Arrays.asList(/* HTTP GET with month and day and possibly throw an exception */); } }
Mari kita tingkatkan kelas Person
kita. Kita mulai dengan menambahkan metode pengujian baru untuk perilaku yang diinginkan dari publishAge()
. Mengapa mulai dengan tes, bukan fungsionalitasnya? Kami mengikuti prinsip pengembangan yang digerakkan oleh pengujian (juga dikenal sebagai TDD), di mana kami menulis pengujian terlebih dahulu, dan kemudian kode untuk membuatnya lulus.
PersonTest.java
// … class PersonTest { // … @Test void testPublishAge() { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate); person.publishAge(); } }
Pada titik ini, kode pengujian gagal dikompilasi karena kita belum membuat metode publishAge()
yang dipanggilnya. Setelah kita membuat metode Person.publishAge()
kosong, semuanya berlalu. Kami sekarang siap untuk tes untuk memverifikasi bahwa usia orang tersebut benar-benar dipublikasikan ke BirthdaysClient
.

Menambahkan Objek Mocked
Karena ini adalah pengujian unit, ia harus berjalan cepat dan dalam memori, sehingga pengujian akan membangun objek Person
kita dengan tiruan BirthdaysClient
sehingga tidak benar-benar membuat permintaan web. Tes kemudian akan menggunakan objek tiruan ini untuk memverifikasi bahwa itu dipanggil seperti yang diharapkan. Untuk melakukan ini, kami akan menambahkan ketergantungan pada kerangka kerja Mockito (lisensi MIT) untuk membuat objek tiruan, dan kemudian membuat objek BirthdaysClient
tiruan:
PersonTest.java
// ... import com.example.birthdays.BirthdaysClient; // ... import static org.mockito.Mockito.mock; class PersonTest { private BirthdaysClient birthdaysClient = mock(BirthdaysClient.class); // ... @Test void testPublishAge() { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); // ... } }
Kami selanjutnya menambah tanda tangan konstruktor Person
untuk mengambil objek BirthdaysClient
, dan mengubah tes untuk menyuntikkan objek BirthdaysClient
yang diejek.
Menambahkan Harapan Palsu
Selanjutnya, kami menambahkan ke akhir testPublishAge
kami harapan bahwa BirthdaysClient
dipanggil. Person.publishAge()
harus memanggilnya, seperti yang ditunjukkan di PersonTest.java
baru kami :
PersonTest.java
// ... class PersonTest { // ... @Test void testPublishAge() throws IOException { // ... Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); verifyZeroInteractions(birthdaysClient); person.publishAge(); verify(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); } }
BirthdaysClient
kami yang disempurnakan dengan Mockito melacak semua panggilan yang telah dilakukan ke metodenya, begitulah cara kami memverifikasi bahwa tidak ada panggilan yang dilakukan ke BirthdaysClient
dengan metode verifyZeroInteractions()
sebelum memanggil publishAge()
. Meskipun bisa dibilang tidak perlu, dengan melakukan ini kami memastikan konstruktor tidak membuat panggilan jahat. Pada baris verify()
, kita menentukan tampilan panggilan ke BirthdaysClient
.
Perhatikan, karena publishRegularPersonAge memiliki IOException di tanda tangannya, kami juga menambahkannya ke tanda tangan metode pengujian kami.
Pada titik ini, tes gagal:
Wanted but not invoked: birthdaysClient.publishRegularPersonAge( "Joe Sixteen", 16L ); -> at com.example.PersonTest.testPublishAge(PersonTest.java:40)
Hal ini diharapkan, mengingat kami belum menerapkan perubahan yang diperlukan pada Person.java
, karena kami mengikuti pengembangan yang digerakkan oleh pengujian. Kami sekarang akan membuat tes ini lulus dengan membuat perubahan yang diperlukan:
orang.java
// ... class Person { // ... private final BirthdaysClient birthdaysClient; Person(String givenName, String surname, LocalDate dateOfBirth) { this(givenName, surname, dateOfBirth, LocalDate::now, new BirthdaysClient()); } // Visible for testing Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier, BirthdaysClient birthdaysClient) { // ... this.birthdaysClient = birthdaysClient; } // ... void publishAge() { String nameToPublish = givenName + " " + surname; long age = getAge(); try { birthdaysClient.publishRegularPersonAge(nameToPublish, age); } catch (IOException e) { // TODO handle this! e.printStackTrace(); } } }
Menguji Pengecualian
Kami membuat konstruktor kode produksi instantiate baru BirthdaysClient
, dan publishAge()
sekarang memanggil birthdaysClient
. Semua tes lulus; semuanya berwarna hijau. Besar! Tetapi perhatikan bahwa publishAge()
menelan IOException. Alih-alih membiarkannya keluar, kami ingin membungkusnya dengan PersonException kami sendiri dalam file baru bernama PersonException.java
:
PersonException.java
package com.example; public class PersonException extends Exception { public PersonException(String message, Throwable cause) { super(message, cause); } }
Kami menerapkan skenario ini sebagai metode pengujian baru di PersonTest.java
:
PersonTest.java
// ... class PersonTest { // ... @Test void testPublishAge_IOException() throws IOException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); IOException ioException = new IOException(); doThrow(ioException).when(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16); try { person.publishAge(); fail("expected exception not thrown"); } catch (PersonException e) { assertSame(ioException, e.getCause()); assertEquals("Failed to publish Joe Sixteen age 16", e.getMessage()); } } }
Panggilan Mockito doThrow()
stubs birthdaysClient
untuk memberikan pengecualian saat metode publishRegularPersonAge()
dipanggil. Jika PersonException
tidak dilempar, kita gagal dalam pengujian. Jika tidak, kami menegaskan bahwa pengecualian dirantai dengan benar dengan IOException dan memverifikasi bahwa pesan pengecualian seperti yang diharapkan. Saat ini, karena kami belum menerapkan penanganan apa pun dalam kode produksi kami, pengujian kami gagal karena pengecualian yang diharapkan tidak dilemparkan. Inilah yang perlu kita ubah di Person.java
untuk membuat tes lulus:
orang.java
// ... class Person { // ... void publishAge() throws PersonException { // ... try { // ... } catch (IOException e) { throw new PersonException("Failed to publish " + nameToPublish + " age " + age, e); } } }
Rintisan: Kapan dan Pernyataan
Kami sekarang mengimplementasikan metode Person.getThoseInCommon()
, membuat kelas Person.Java
kami terlihat seperti ini.
testGetThoseInCommon()
kami, tidak seperti testPublishAge()
, tidak memverifikasi bahwa panggilan tertentu dibuat ke metode birthdaysClient
. Alih-alih, ia menggunakan when
panggilan ke stub mengembalikan nilai untuk panggilan ke findFamousNamesOfAge()
dan findFamousNamesBornOn()
yang harus dibuat oleh getThoseInCommon()
. Kami kemudian menegaskan bahwa ketiga nama stub yang kami berikan dikembalikan.
Membungkus beberapa pernyataan dengan metode assertAll()
JUnit 5 memungkinkan semua pernyataan diperiksa secara keseluruhan, daripada berhenti setelah pernyataan gagal pertama. Kami juga menyertakan pesan dengan assertTrue()
untuk mengidentifikasi nama tertentu yang tidak disertakan. Inilah yang terlihat seperti metode pengujian "jalur bahagia" (skenario ideal) kami (perhatikan, ini bukan serangkaian pengujian yang kuat secara alami sebagai "jalur bahagia", tetapi kami akan membicarakan alasannya nanti.
PersonTest.java
// ... class PersonTest { // ... @Test void testGetThoseInCommon() throws IOException, PersonException { LocalDate dateOfBirth = LocalDate.parse("2000-01-02"); LocalDate currentDate = LocalDate.parse("2017-01-01"); Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient); when(birthdaysClient.findFamousNamesOfAge(16)).thenReturn(Arrays.asList("JoeFamous Sixteen", "Another Person")); when(birthdaysClient.findFamousNamesBornOn(1, 2)).thenReturn(Arrays.asList("Jan TwoKnown")); Set<String> thoseInCommon = person.getThoseInCommon(); assertAll( setContains(thoseInCommon, "Another Person"), setContains(thoseInCommon, "Jan TwoKnown"), setContains(thoseInCommon, "JoeFamous Sixteen"), ()-> assertEquals(3, thoseInCommon.size()) ); } private <T> Executable setContains(Set<T> set, T expected) { return () -> assertTrue(set.contains(expected), "Should contain " + expected); } // ... }
Jaga Kode Tes Tetap Bersih
Meskipun sering diabaikan, sama pentingnya untuk menjaga kode pengujian bebas dari duplikasi yang membusuk. Kode bersih dan prinsip-prinsip seperti "jangan ulangi diri Anda sendiri" sangat penting untuk mempertahankan basis kode, produksi, dan kode pengujian yang berkualitas tinggi. Perhatikan bahwa PersonTest.java terbaru memiliki beberapa duplikasi sekarang karena kami memiliki beberapa metode pengujian.
Untuk memperbaikinya, kita dapat melakukan beberapa hal:
Ekstrak objek IOException ke bidang final pribadi.
Ekstrak pembuatan objek
Person
ke dalam metodenya sendiri (createJoeSixteenJan2()
, dalam kasus ini) karena sebagian besar objek Person dibuat dengan parameter yang sama.Buat
assertCauseAndMessage()
untuk berbagai pengujian yang memverifikasiPersonExceptions
yang dilempar.
Hasil kode bersih dapat dilihat pada rendisi file PersonTest.java ini.
Uji Lebih Dari Jalan Bahagia
Apa yang harus kita lakukan ketika objek Person
memiliki tanggal lahir yang lebih lambat dari tanggal saat ini? Cacat dalam aplikasi sering kali disebabkan oleh input yang tidak terduga atau kurangnya pandangan ke depan terhadap kasus sudut, tepi, atau batas. Penting untuk mencoba mengantisipasi situasi ini sebaik mungkin, dan tes unit sering kali merupakan tempat yang tepat untuk melakukannya. Dalam membangun Person
dan PersonTest
kami, kami menyertakan beberapa tes untuk pengecualian yang diharapkan, tetapi itu tidak berarti lengkap. Misalnya, kami menggunakan LocalDate
yang tidak mewakili atau menyimpan data zona waktu. Panggilan kami ke LocalDate.now()
, bagaimanapun, mengembalikan LocalDate
berdasarkan zona waktu default sistem, yang bisa sehari lebih awal atau lebih lambat dari pengguna sistem. Faktor-faktor ini harus dipertimbangkan dengan tes yang sesuai dan perilaku yang diterapkan.
Batas juga harus diuji. Pertimbangkan objek Person
dengan metode getDaysUntilBirthday()
. Pengujian harus mencakup apakah ulang tahun orang tersebut telah berlalu atau tidak pada tahun ini, apakah hari ulang tahun orang tersebut hari ini, dan bagaimana tahun kabisat memengaruhi jumlah hari. Skenario ini dapat dicakup dengan memeriksa satu hari sebelum ulang tahun orang tersebut, hari, dan satu hari setelah ulang tahun orang tersebut di mana tahun berikutnya adalah tahun kabisat. Berikut adalah kode tes yang relevan:
PersonTest.java
// ... class PersonTest { private final Supplier<LocalDate> currentDateSupplier = ()-> LocalDate.parse("2015-05-02"); private final LocalDate ageJustOver5 = LocalDate.parse("2010-05-01"); private final LocalDate ageExactly5 = LocalDate.parse("2010-05-02"); private final LocalDate ageAlmost5 = LocalDate.parse("2010-05-03"); // ... @Test void testGetDaysUntilBirthday() { assertAll( createPersonAndAssertValue(ageAlmost5, 1, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageExactly5, 0, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageJustOver5, 365, Person::getDaysUntilBirthday) ); } private Executable createPersonAndAssertValue(LocalDate dateOfBirth, long expectedValue, Function<Person, Long> personLongFunction) { Person person = new Person("Given", "Sur", dateOfBirth, currentDateSupplier); long actualValue = personLongFunction.apply(person); return () -> assertEquals(expectedValue, actualValue); } }
Tes Integrasi
Kami sebagian besar berfokus pada pengujian unit, tetapi JUnit juga dapat digunakan untuk pengujian integrasi, penerimaan, fungsional, dan sistem. Pengujian semacam itu sering kali memerlukan lebih banyak kode penyiapan, misalnya memulai server, memuat basis data dengan data yang diketahui, dll. Meskipun kita sering kali dapat menjalankan ribuan pengujian unit dalam hitungan detik, rangkaian pengujian integrasi besar mungkin memerlukan beberapa menit atau bahkan berjam-jam untuk dijalankan. Tes integrasi umumnya tidak boleh digunakan untuk mencoba mencakup setiap permutasi atau jalur melalui kode; unit test lebih tepat untuk itu.
Membuat pengujian untuk aplikasi web yang menggerakkan browser web dalam mengisi formulir, mengklik tombol, menunggu konten dimuat, dll., biasanya dilakukan menggunakan Selenium WebDriver (lisensi Apache 2.0) yang digabungkan dengan 'Page Object Pattern' (lihat wiki github SeleniumHQ dan artikel Martin Fowler tentang Objek Halaman).
JUnit efektif untuk menguji RESTful API dengan menggunakan klien HTTP seperti Apache HTTP Client atau Spring Rest Template (HowToDoInJava.com memberikan contoh yang baik).
Dalam kasus kami dengan objek Person
, tes integrasi dapat melibatkan penggunaan BirthdaysClient
asli daripada tiruan, dengan konfigurasi yang menentukan URL dasar layanan People Birthdays. Tes integrasi kemudian akan menggunakan contoh uji layanan semacam itu, memverifikasi bahwa ulang tahun diterbitkan untuk itu dan membuat orang-orang terkenal di layanan yang akan dikembalikan.
Fitur JUnit Lainnya
JUnit memiliki banyak fitur tambahan yang belum kami jelajahi dalam contoh. Kami akan menjelaskan beberapa dan memberikan referensi untuk yang lain.
Perlengkapan Tes
Perlu dicatat bahwa JUnit membuat instance baru dari kelas pengujian untuk menjalankan setiap metode @Test
. JUnit juga menyediakan kait anotasi untuk menjalankan metode tertentu sebelum atau sesudah semua atau setiap metode @Test
. Kait ini sering digunakan untuk menyiapkan atau membersihkan database atau objek tiruan, dan berbeda antara JUnit 4 dan 5.
Juni 4 | JUnit 5 | Untuk Metode Statis? |
---|---|---|
@BeforeClass | @BeforeAll | Ya |
@AfterClass | @AfterAll | Ya |
@Before | @BeforeEach | Tidak |
@After | @AfterEach | Tidak |
Dalam contoh PersonTest
kami, kami memilih untuk mengonfigurasi objek tiruan BirthdaysClient
dalam metode @Test
itu sendiri, tetapi terkadang struktur tiruan yang lebih kompleks perlu dibangun dengan melibatkan banyak objek. @BeforeEach
(dalam JUnit 5) dan @Before
(dalam JUnit 4) sering sesuai untuk ini.
Anotasi @After*
lebih umum dengan pengujian integrasi daripada pengujian unit karena pengumpulan sampah JVM menangani sebagian besar objek yang dibuat untuk pengujian unit. @BeforeClass
dan @BeforeAll
paling sering digunakan untuk pengujian integrasi yang perlu melakukan tindakan penyiapan dan pembongkaran yang mahal sekali, bukan untuk setiap metode pengujian.
Untuk JUnit 4, silakan merujuk ke panduan perlengkapan tes (konsep umum masih berlaku untuk JUnit 5).
Test Suite
Terkadang Anda ingin menjalankan beberapa pengujian terkait, tetapi tidak semua pengujian. Dalam hal ini, pengelompokan tes dapat disusun menjadi rangkaian tes. Untuk cara melakukannya di JUnit 5, lihat artikel JUnit 5 HowToProgram.xyz, dan di dokumentasi tim JUnit untuk JUnit 4.
@Nested dan @DisplayName JUnit 5
JUnit 5 menambahkan kemampuan untuk menggunakan kelas dalam bersarang non-statis untuk menunjukkan hubungan antar pengujian dengan lebih baik. Ini seharusnya sangat akrab bagi mereka yang telah bekerja dengan deskripsi bersarang dalam kerangka pengujian seperti Jasmine untuk JavaScript. Kelas dalam dijelaskan dengan @Nested
untuk menggunakan ini.
Anotasi @DisplayName
juga baru di JUnit 5, memungkinkan Anda mendeskripsikan pengujian untuk pelaporan dalam format string, yang akan ditampilkan selain pengidentifikasi metode pengujian.
Meskipun @Nested
dan @DisplayName
dapat digunakan secara terpisah satu sama lain, keduanya dapat memberikan hasil pengujian yang lebih jelas yang menggambarkan perilaku sistem.
Pencocok Hamcrest
Kerangka kerja Hamcrest, meskipun bukan merupakan bagian dari basis kode JUnit, memberikan alternatif untuk menggunakan metode penegasan tradisional dalam pengujian, memungkinkan kode pengujian yang lebih ekspresif dan mudah dibaca. Lihat verifikasi berikut menggunakan baik asserEquals tradisional dan asert Hamcrest That:
//Traditional assert assertEquals("Hayden, Josh", displayName); //Hamcrest assert assertThat(displayName, equalTo("Hayden, Josh"));
Hamcrest dapat digunakan dengan JUnit 4 dan 5. Tutorial Vogella.com tentang Hamcrest cukup lengkap.
Sumber daya tambahan
Artikel Unit Tests, How to Write Testable Code and Why it Matters mencakup contoh yang lebih spesifik dari penulisan kode yang bersih dan dapat diuji.
Bangun dengan Percaya Diri: Panduan untuk Pengujian JUnit memeriksa berbagai pendekatan untuk pengujian unit dan integrasi, dan mengapa yang terbaik adalah memilih satu dan tetap menggunakannya
Panduan Pengguna JUnit 4 Wiki dan JUnit 5 selalu menjadi titik referensi yang sangat baik.
Dokumentasi Mockito memberikan informasi tentang fungsionalitas dan contoh tambahan.
JUnit adalah Jalan Menuju Otomatisasi
Kami telah menjelajahi banyak aspek pengujian di dunia Java dengan JUnit. Kami telah melihat pengujian unit dan integrasi menggunakan kerangka kerja JUnit untuk basis kode Java, mengintegrasikan JUnit dalam lingkungan pengembangan dan pembangunan, cara menggunakan tiruan dan rintisan dengan pemasok dan Mockito, konvensi umum dan praktik kode terbaik, apa yang harus diuji, dan beberapa fitur JUnit hebat lainnya.
Sekarang giliran pembaca untuk berkembang dalam menerapkan, memelihara, dan menuai manfaat dari pengujian otomatis menggunakan kerangka kerja JUnit dengan terampil.