Day 2: Test-Driven Development
- Session 1: 09:00 - 10:00: Testing Category
- Session 2: 10:15 - 11:45: Hands-on: A bigger Case study
- Session 3: 13:00 - 15:00: Hands-on: A bigger Case study
- Session 4: 15:30 - 16:30: Overview, Discussion, Lesson learned
Dive Deeper into Unit Testing
Pada hari pertama, telah disediakan contoh bagaimana membuat aplikasi web berbasis Spring Boot dengan unit test dan functional test suite yang lengkap. Pada hari ini, kita akan mendalami terkait bagaimana membuat unit test suite yang memenuhi 5 aspek FIRST principle:
- Fast: proses testing secara keseluruhan harus dilakukan dengan cepat.
- Isolated: setiap unit test tidak boleh memengaruhi hasil unit test lain.
- Repeatable: hasil tes konsisten meskipun dijalankan berkali-kali pada kondisi yang sama.
- Self-Validating: bisa digunakan untuk mengecek kesesuaian aplikasi jika terjadi perubahan kode.
- Thorough: sebisa mungkin mencakup keseluruhan kode dan business logic aplikasi.
Untuk hands-on ini, kita akan fokus ke sisi back-end dari aplikasi. Kita akan memanfaatkan teknik berupa mock dan stub untuk membuat kode cakupan setiap test kita menjadi lebih fokus terhadap objek yang benar-benar akan di-test.
Apa itu mock?
Mock adalah objek palsu yang kita bisa gunakan untuk menggunakan objek yang menjadi dependensi dari suatu fungsi/kelas. Mock object dapat kita gunakan untuk tracking, yaitu melihat bagaimana interaksi antara fungsi yang kita test dengan objek dependensi tersebut.
Contoh sederhananya adalah, kita ingin melihat apakah fungsi kita melakukan proses Save Object ke sebuah database. Berikut adalah contoh penggunaan mock di test code:
1 2 3 4 5 6 7 8 9 10 11 12 | |
Apa itu stub?
Stub adalah objek palsu yang kita bisa gunakan untuk menyimulasikan keluaran fungsi-fungsi pada objek tersebut. Hal ini sangat berguna jika kita menggunakan library eksternal atau API eksternal, sehingga unit test kita tidak akan membuang waktu dan resource untuk mengakses library atau API tersebut. Kita bisa meminta stub untuk mengembalikan hasil yang kita inginkan untuk diproses oleh fungsi yang kita test. Kita juga bisa meminta stub untuk memberikan error untuk menyimulasikan negative case.
Contoh sederhananya adalah, kita ingin menyimulasikan bahwa terdapat objek Payment di database, tanpa kita harus memasukkan entri Payment ke dalam database terlebih dahulu. Berikut adalah contoh penggunaan stub di test code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
Persiapan Workshop
Untuk mengikuti rangkaian kegiatan workshop hari ini, harap persiapkan tools berikut di komputer anda:
- Git
- Java JDK 17 (
javadanjavac) - IntelliJ IDEA Community Edition
- Apache Maven (
mvn)
Buat salinan branch main dari repositori Git kode templat workshop hari ini:
1 | |
Apabila sudah menyalin repositori Git dan telah menyiapkan tools yang dibutuhkan, Anda bisa menjalanakan aplikasi contoh dengan perintah shell berikut:
1 2 3 4 | |
Perlu diketahui bahwa aplikasi yang ada pada repo tersebut belum lengkap. Kita akan lengkapi bersama saat tutorial ini.
SIBAYAR: Pembayaran peer-to-peer dengan mekanisme Accept Payment dan Disbursement
SIBAYAR merupakan sistem pembayaran peer-to-peer sederhana yang menggunakan sebuah API payment gateway (dalam kasus ini, Flip) untuk menyalurkan uang. Berikut adalah flow keseluruhan dari sistem SIBAYAR:
- User bisa melakukan transfer uang ke User lain, atau ke nomor rekening yang tidak terdaftar di SIBAYAR.
- SIBAYAR mengontak API Flip untuk mendapatkan link pembayaran, yang bisa digunakan User untuk membayar transfer tersebut.
- User melakukan pembayaran menggunakan link pembayaran yang telah diberikan API Flip ke User melalui SIBAYAR.
- Jika user sudah selesai melakukan pembayaran, API Flip akan mengeksekusi payment callback endpoint milik SIBAYAR untuk melanjutkan proses transfer.
- Ketika API Flip mengeksekusi payment callback endpoint SIBAYAR, sistem SIBAYAR akan kembali mengontak API Flip untuk transfer uang (disbursement) ke rekening tujuan.
- Ketika Flip berhasil melakukan disbursement, API Flip akan mengeksekusi disbursement callback endpoint milik SIBAYAR, sehingga pembayaran tersebut akan ditandai sukses oleh SIBAYAR.
Untuk tutorial hari ini, tidak perlu khawatirkan akses callback dari Flip, karena Flip hanya bisa melakukan callback ketika aplikasi SIBAYAR sudah di-deploy secara publik. Akan tetapi, kita tetap perlu mengimplementasikan keseluruhan sistem SIBAYAR, dengan menyusun tiga controller:
AuthenticationController: untuk proses login dan register akun baru.PaymentController: untuk melakukan payment baru dan melihat histori payment.CallbackController: untuk payment callback endpoint dan disbursement callback endpoint yang akan dikontak oleh API Flip.
Membuat Test Suite Class
Untuk proses pembuatan unit test, kita akan memanfaatkan library Mockito.
Mockito berfungsi untuk membuat mock dan stub serta meng-inject objek-objek palsu tersebut (dependency injection) ke dalam objek yang akan kita test.
Untuk menyusun test suite yang support Mockito,
kita perlu tambahkan anotasi @ExtendWith(MockitoExtension.class).
Selain itu, kita perlu membuat fungsi setUp() dengan anotasi @BeforeEach, yang berfungsi sebagai prosedur set up untuk setiap unit test pada test suite.
Fungsi setUp() yang dianotasi dengan @BeforeEach akan dijalankan sebelum setiap unit test dijalankan.
Dengan fungsi setUp(), kita bisa mengisolasi proses inisiasi setiap unit test agar hasilnya bisa independen,
selain itu juga meningkatkan reusability dan konsistensi antar test case karena menggunakan cara inisiasi yang serupa.
Berikut adalah contoh inisiasi test suite untuk class PaymentServiceImpl, yaitu class PaymentServiceImplTest yang dibuat di folder src/test/com/example/sibayar/service/payment:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | |
Dalam kasus ini, terdapat 3 objek yang akan dibuat objek mock/stub-nya, yang ditandai dengan anotasi @Mock:
PaymentRepository: sebuah class yang bertugas untuk melakukan operasi database terhadap tabelPayment. Di Spring Boot, class ini disebut sebagai repository.UserRepository: sebuah class yang bertugas untuk melakukan operasi database terhadap tabelUser. Di Spring Boot, class ini disebut sebagai repository.User: sebuah class yang bertugas sebagai database model untuk tabel User (pengguna).
Ketiga objek ini kemudian akan di-inject ke PaymentServiceImpl yang akan kita test, ditandai dengan anotasi @InjectMocks.
Terdapat juga 2 objek yang akan menjadi parameter dari fungsi yang akan di-test, yaitu:
PaymentToUserRequest: sebuah Data Transfer Object (DTO) sebagai argumen dari fungsipayToUser, yang datang dariPaymentController.PaymentToOtherRequest: sebuah Data Transfer Object (DTO) sebagai argumen dari fungsipayToOtherDestination, yang datang dariPaymentController.
Terdapat juga 2 objek yang akan menjadi hasil dari stub, yaitu:
Payment: sebuah objek database model untuk tabel Payment.PaymentLinkResponse: sebuah objek Data Transfer Object (DTO) untuk menampung hasil kembalian dari Flip API ketika membuat payment link.
Tugas Anda
- Buat test suite
PaymentServiceImplTestsesuai dengan arahan yang diberikan.- Test suite terdiri dari objek mock/stub berikut:
PaymentRepositoryUserRepositoryUser
- Test suite terdiri dari objek untuk parameter fungsi yang di-test berikut:
PaymentToUserRequestPaymentToOtherRequest
- Test suite terdiri dari objek hasil stub berikut:
PaymentPaymentLinkResponse
- Buat fungsi
setUp()untuk menyusun objek-objek hasil stub.
- Test suite terdiri dari objek mock/stub berikut:
Latihan Mandiri: Inisiasi test suite CallbackServiceImplTest
- Buat test suite
CallbackServiceImplTest, sejajar dengan test suitePaymentServiceImplTestyang telah dibuat.- Test suite terdiri dari objek mock/stub berikut:
PaymentRepository
- Test suite terdiri dari objek untuk parameter fungsi yang di-test berikut:
PaymentCallbackRequestDisbursementCallbackRequest
- Test suite terdiri dari objek hasil stub berikut:
PaymentDisbursementResponse
- Buat fungsi
setUp()untuk menyusun objek-objek hasil stub.
- Test suite terdiri dari objek mock/stub berikut:
Melakukan Mock dan Stub untuk Akses Database dan Helper Function Lain
SIBAYAR menggunakan Spring Data JPA untuk mengakses database In-Memory H2.
Dalam menggunakan Spring Data JPA, kita perlu membuat repository interface yang berisikan daftar method untuk mengakses database.
JPA kemudian akan membuatkan implementasi setiap method secara on the fly sesuai dengan nama method yang kita gunakan.
Repository interface tersebut akan menjadi sebuah komponen Spring yang bisa digunakan untuk service yang akan kita buat bersama.
Di tutorial hari ini, sudah tersedia repository untuk tabel database Payment yaitu PaymentRepository, dan tabel database User yaitu UserRepository.
Database adalah komponen dependensi pada objek service yang perlu kita isolasi. Programmer pada umumnya akan membuat entri ke database lalu akan menghapusnya kembali ketika test selesai. Akan tetapi, hal tersebut jadi memakan resource lebih dengan perlu adanya testing database yang nyata. Oleh karena itu, dalam menyusun unit test untuk service, kita perlu melakukan mock dan stub komponen Repository. Sebagai contoh, berikut adalah beberapa test case yang bisa Anda gunakan:
Contoh 1: Fungsi payToUser sukses menyimpan dan mengembalikan objek Payment baru
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Berikut adalah penjelasan terkait test case ini:
- Di line 3, terdapat stubbing untuk fungsi
userRepository.findById(Integer)untuk menyimulasikan bahwa di database terdapat objek mockuser. - Di line 4, terdapat stubbing untuk fungsi
service.getPaymentLink(PaymentLinkRequest)untuk mengembalikan objekpaymentLinkResponse, untuk menyimulasikan kembalian dari pemanggilan API Flip untuk mendapatkan payment link.
Test case ini akan mengecek:
- Apakah fungsi
payToUsermemanggil setidaknya satu kali helper functiongetPaymentLinkuntuk akses API Flip? (sintaks verifikasi pemanggilan mock ada di line 8) - Apakah fungsi
payToUsermemanggil setidaknya satu kali fungsisavepadaPaymentRepositoryuntuk menyimpan objekPaymentke dalam database? (sintaks verifikasi pemanggilan mock ada di line 9) - Apakah fungsi
payToUsermengembalikan objekPayment, dengan isi yang sama seperti yang dikembalikan oleh API Flip?
Contoh 2: Fungsi paymentCallback sukses menyimpan status WAITING_DISBURSEMENT ketika status Payment Link "SUCCESSFUL" dan status Disbursement "PENDING"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Fungsi paymentCallback bertujuan untuk meneruskan pembayaran lewat mekanisme Disbursement jika status Payment Link yang diberikan "SUCCESSFUL".
Berikut adalah penjelasan terkait test case ini:
- Di line 3, terdapat stubbing untuk fungsi
paymentRepository.findByPaymentLinkId(Integer)untuk menyimulasikan bahwa di database terdapat objekpayment. - Di line 4, dilakukan set status pada objek
paymentCallbackRequestmenjadi"SUCCESSFUL", untuk menyimulasikan API Flip memanggil payment callback dengan status "sukses" sehingga proses disbursement bisa dilakukan. - Di line 5, terdapat stubbing untuk fungsi
service.disburseMoney(DisbursementRequest)untuk mengembalikan objekpaymentLinkResponse. - Di line 6, dilakukan set status pada objek
disbursementResponsemenjadi"PENDING"untuk menyimulasikan API Flip sedang melakukan proses disbursement.
Test case ini akan mengecek:
- Apakah fungsi
paymentCallbackmemanggil setidaknya satu kali helper functiondisburseMoneyuntuk akses API Flip? (sintaks verifikasi pemanggilan mock ada di line 10) - Apakah fungsi
paymentCallbackmemanggil setidaknya satu kali fungsisavepadaPaymentRepositoryuntuk menyimpan perubahan objekPaymentke dalam database? (sintaks verifikasi pemanggilan mock ada di line 11) - Apakah fungsi
paymentCallbackmengembalikan objekPaymentdenganstatusberupa"WAITING_DISBURSEMENT"?
Contoh 3: Fungsi paymentCallback sukses menyimpan status WAITING_DISBURSEMENT ketika status Payment Link "SUCCESSFUL" dan status Disbursement "DONE"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Berikut adalah penjelasan terkait test case ini:
- Di line 3, terdapat stubbing untuk fungsi
paymentRepository.findByPaymentLinkId(Integer)untuk menyimulasikan bahwa di database terdapat objekpayment. - Di line 4, dilakukan set status pada objek
paymentCallbackRequestmenjadi"SUCCESSFUL", untuk menyimulasikan API Flip memanggil payment callback dengan status "sukses" sehingga proses disbursement bisa dilakukan. - Di line 5, terdapat stubbing untuk fungsi
service.disburseMoney(DisbursementRequest)untuk mengembalikan objekpaymentLinkResponse. - Di line 6, dilakukan set status pada objek
disbursementResponsemenjadi"DONE", untuk menyimulasikan API Flip langsung selesai melakukan disbursement.
Test case ini akan mengecek:
- Apakah fungsi
paymentCallbackmemanggil setidaknya satu kali helper functiondisburseMoneyuntuk akses API Flip? (sintaks verifikasi pemanggilan mock ada di line 10) - Apakah fungsi
paymentCallbackmemanggil setidaknya satu kali fungsisavepadaPaymentRepositoryuntuk menyimpan perubahan objekPaymentke dalam database? (sintaks verifikasi pemanggilan mock ada di line 11) - Apakah fungsi
paymentCallbackmengembalikan objekPaymentdenganstatusberupa"SUCCESS"?
Tugas Anda
- Buat test case
testPayToUserSuccesssesuai dengan arahan yang sudah diberikan.- Test case diletakkan di dalam test suite
com.example.sibayar.service.payment.PaymentServiceImplTest. - Pastikan test case mengecek tiga hal yang disebutkan di arahan.
- Test case diletakkan di dalam test suite
- Buat test case
testPaymentCallbackWithSuccessfulStatusAndPendingDisbursementsesuai dengan arahan yang sudah diberikan.- Test case diletakkan di
com.example.sibayar.service.payment.CallbackServiceImplTest. - Pastikan test case mengecek tiga hal yang disebutkan di arahan.
- Test case diletakkan di
- Buat test case
testPaymentCallbackWithSuccessfulStatusAndSuccessfulDisbursementsesuai dengan arahan yang sudah diberikan.- Test case diletakkan di
com.example.sibayar.service.payment.CallbackServiceImplTest. - Pastikan test case mengecek tiga hal yang disebutkan di arahan.
- Test case diletakkan di
Latihan Mandiri: Buat sebuah positive case
- Buat test case
testPayToOtherSuccesspada test suitePaymentServiceImplTestuntuk mengecek:- Apakah fungsi
payToOtherDestinationmemanggil helper functiongetPaymentLinkuntuk akses API Flip? - Apakah fungsi
payToOtherDestinationmemanggil fungsisavepadaPaymentRepositoryuntuk menyimpan objekPaymentke dalam database? - Apakah fungsi
payToOtherDestinationmengembalikan objekPayment, dengan isi yang sama seperti yang dikembalikan oleh API Flip?
- Apakah fungsi
Latihan Mandiri: Buat beberapa negative case
- Buat test case
testPaymentCallbackWithSuccessfulStatusButCancelledDisbursementpada test suiteCallbackServiceImplTestuntuk mengecek:- Apakah fungsi
paymentCallbackmemanggil setidaknya satu kali helper functiondisburseMoneyuntuk akses API Flip? - Apakah fungsi
paymentCallbackmemanggil setidaknya satu kali fungsisavepadaPaymentRepositoryuntuk menyimpan perubahan objekPaymentke dalam database? - Apakah fungsi
paymentCallbackmengembalikan objekPaymentdenganstatusberupa"DISBURSEMENT_FAILED"? - CATATAN:
statuspadadisbursementResponseharus diganti dengan"CANCELLED".
- Apakah fungsi
- Buat test case
testPaymentCallbackWithCancelledStatuspada test suiteCallbackServiceImplTestuntuk mengecek:- Apakah fungsi
paymentCallbackTIDAK memanggil helper functiondisburseMoneyuntuk akses API Flip?
HINT: gunakanuntuk mengecek bahwa helper function tidak dipanggil.1verify(service, atMost(0)).disburseMoney(any(DisbursementRequest.class)) - Apakah fungsi
paymentCallbackmemanggil setidaknya satu kali fungsisavepadaPaymentRepositoryuntuk menyimpan perubahan objekPaymentke dalam database? - Apakah fungsi
paymentCallbackmengembalikan objekPaymentdenganstatusberupa"PAYMENT_FAILED"? - CATATAN:
statuspadapaymentCallbackRequestharus diganti dengan"CANCELLED".
- Apakah fungsi
Menguji Apakah Fungsi Mengeluarkan Exception dalam Suatu Test Case
Unit test tidak hanya mencakup positive case (dalam kasus ini: mengembalikan objek Payment),
akan tetapi juga perlu mencakup negative case (dalam kasus ini: mengeluarkan sebuah exception).
Untuk mengecek apakah aplikasi mengeluarkan exception, kita bisa memanfaatkan fungsi assertThrows.
Sebagai contoh, berikut adalah beberapa negative case yang bisa Anda gunakan:
Contoh 1: payToUser pada PaymentServiceImpl akan mengembalikan error ketika user ID dari sender dan destination sama.
1 2 3 4 5 6 | |
Test case ini akan mengecek apakah exception SelfPaymentException dikeluarkan jika user ID dari sender dan destination sama.
Contoh 2: payToUser pada PaymentServiceImpl akan mengembalikan error jika Flip API sedang tidak bisa diakses.
1 2 3 4 5 6 7 8 9 10 | |
Berikut adalah penjelasan mengenai stubbing pada test case ini:
- Di line 3, terdapat stubbing untuk fungsi
userRepository.findById(Integer)untuk mengembalikan objek mockuser. - Di line 4, terdapat stubbing untuk fungsi
service.getPaymentLink(PaymentLinkRequest)untuk melempar exceptionAPIUnreachableException.
Test case ini akan mengecek:
- Apakah fungsi
payToUsermeneruskan exceptionAPIUnreachableException? (pengecekan di line 6) - Apakah fungsi
payToUsermemanggil helper functiongetPaymentLinkuntuk akses API Flip? (sintaks verifikasi pemanggilan mock ada di line 9)
Contoh 3: paymentCallback pada CallbackServiceImpl akan mengembalikan error jika pembayaran lewat payment link sukses, namun Flip API untuk disbursement sedang tidak bisa diakses.
1 2 3 4 5 6 7 8 9 10 11 | |
Berikut adalah penjelasan mengenai stubbing pada test case ini:
- Di line 3, terdapat stubbing untuk fungsi
paymentRepository.findByPaymentLinkId(Integer)untuk mengembalikan objekpayment. - Di line 4, dilakukan set status pada objek
paymentCallbackRequestmenjadi"SUCCESSFUL", karena akses API Flip untuk disbursement hanya akan diakses jika status payment link sudahSUCCESSFUL. - Di line 5, Selain itu, juga terdapat stubbing untuk fungsi
service.disburseMoney(DisbursementRequest)untuk melempar exceptionAPIUnreachableException. Ini untuk menyimulasikan ketika API Flip sedang tidak bisa diakses.
Test case ini akan mengecek:
- Apakah fungsi
payToUsermeneruskan exceptionAPIUnreachableException? (pengecekan di line 8) - Apakah fungsi
payToUsermemanggil helper functiondisburseMoneyuntuk akses API Flip? (sintaks verifikasi pemanggilan mock ada di line 11)
Tugas Anda
- Buat test case
testPayToUserFailWhenSenderAndDestinationAreTheSamesesuai dengan arahan yang sudah diberikan.- Test case diletakkan di dalam test suite
com.example.sibayar.service.payment.PaymentServiceImplTest. - Pastikan test case mengecek exception yang di-throw seperti yang disebutkan di arahan.
- Test case diletakkan di dalam test suite
- Buat test case
testPayToUserThrowsExceptionWhenAPIIsUnreachablesesuai dengan arahan yang sudah diberikan.- Test case diletakkan di
com.example.sibayar.service.payment.PaymentServiceImplTest. - Pastikan test case mengecek tiga hal yang disebutkan di arahan.
- Test case diletakkan di
- Buat test case
testPaymentCallbackWithSuccessfulStatusThrowsExceptionWhenAPIIsUnreachablesesuai dengan arahan yang sudah diberikan.- Test case diletakkan di
com.example.sibayar.service.payment.CallbackServiceImplTest. - Pastikan test case mengecek tiga hal yang disebutkan di arahan.
- Test case diletakkan di
Latihan Mandiri: Buat negative case
- Buat test case
testPayToOtherThrowsExceptionWhenAPIIsUnreachablepada test suitePaymentServiceImplTestuntuk mengecek:- Apakah fungsi
payToOtherDestinationmeneruskan exceptionAPIUnreachableException? - Apakah fungsi
payToOtherDestinationmemanggil setidaknya satu kali helper functiongetPaymentLinkuntuk akses API Flip?
- Apakah fungsi
Refactoring: Bagaimana jika SIBAYAR bisa menggunakan lebih dari satu Payment Gateway?
Ternyata kode SIBAYAR yang asli belum cukup modular.
Bagaimana jika suatu hari kita tidak menggunakan Flip sebagai payment gateway?
Salah satu cara yang dapat kita lakukan adalah memisahkan helper function getPaymentLink dan disburseMoney menjadi sebuah class tersendiri di bawah package com.example.sibayar.external.paymentgateway.
Lalu, bagaimana cara kita menyesuaikan implementasi dan unit test kita ketika ada perubahan desain tersebut?
Pada bagian ini, kita akan melakukan proses refactoring beserta penyesuaian unit test yang perlu dilakukan.
Tugas Anda
- Buat interface baru
PaymentGatewayAPIdi packagecom.example.sibayar.external.paymentgateway, dengan menggunakan snippet berikut:Tujuan dari interface1 2 3 4
public interface PaymentGatewayAPI { PaymentLinkResponse getPaymentLink(PaymentLinkRequest request); DisbursementResponse disburseMoney(DisbursementRequest request); }PaymentGatewayAPIadalah untuk memastikan bahwa semua class penghubung dengan API payment gateway dapat diakses dengan cara yang sama. -
Buat class baru
FlipAPIdi packakgecom.example.sibayar.external.paymentgateway, dengan menggunakan snippet berikut:Lakukan juga beberapa hal berikut:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
@Service("flipAPI") @RequiredArgsConstructor public class FlipAPI implements PaymentGatewayAPI { @Value("${sibayar.flip.baseUrl}") private String baseUrl; @Value("${sibayar.flip.apiKey}") private String apiKey; private HttpClient client = HttpClient.newHttpClient(); public PaymentLinkResponse getPaymentLink(PaymentLinkRequest request) { // TODO: Pindahkan isi fungsi getPaymentLink di PaymentServiceImpl ke sini. } public DisbursementResponse disburseMoney(DisbursementRequest request) { // TODO: Pindahkan isi fungsi disburseMoney di CallbackServiceImpl ke sini. } private String getBasicAuthHeader(String username, String password) { String valueToEncode = username + ":" + password; return "Basic " + Base64.getEncoder() .encodeToString(valueToEncode.getBytes()); } }- Pindahkan juga isi method
getPaymentLinkyang sebelumnya ada dicom.example.sibayar.service.payment.PaymentServiceImpl, ke dalam methodgetPaymentLinkyang ada diFlipAPI. - Pindahkan juga isi method
disburseMoneyyang sebelumnya ada dicom.example.sibayar.service.payment.CallbackServiceImpl, ke dalam methodgetPaymentLinkyang ada diFlipAPI.
- Pindahkan juga isi method
-
Pada
PaymentServiceImpldanCallbackServiceImpl, buat koneksi baru ke implementasi dariPaymentGatewayAPIdengan cara membuat instance variable baru. Gunakan snippet berikut untuk diletakkan di definisi class dariPaymentServiceImpldanCallbackServiceImpl.1 2
@Qualifier("flipAPI") private final PaymentGatewayAPI api;@Qualifier("flipAPI")akan otomatis melakukan dependency injection sehingga variabelapiakan berisi objek dariFlipAPI. - Gunakan
apiuntuk mengakses fungsigetPaymentLinkdandisburseMoneyyang telah dipindahkan keFlipAPI. Misal dari sebelumnya:menjadi seprti berikut:1PaymentLinkResponse apiResponse = getPaymentLink(apiRequest);1PaymentLinkResponse apiResponse = api.getPaymentLink(apiRequest); - Tambahkan objek mock untuk
PaymentGatewayAPIpada test suitePaymentServiceImplTestdanCallbackServiceImplTestdengan menggunakan snippet berikut:1 2
@Mock private PaymentGatewayAPI flipAPI; - Gunakan objek mock
flipAPIuntuk menggantikan pemanggilan fungsigetPaymentLinkdandisburseMoneypada setiap test case. Misal dari sebelumnya:menjadi seperti berikut:1 2 3
when(service.disburseMoney(any(DisbursementRequest.class))).thenReturn(disbursementResponse); // ... verify(service, atLeastOnce()).disburseMoney(any(DisbursementRequest.class));1 2 3
when(flipAPI.disburseMoney(any(DisbursementRequest.class))).thenReturn(disbursementResponse); // ... verify(flipAPI, atLeastOnce()).disburseMoney(any(DisbursementRequest.class));
Latihan Mandiri Tambahan
- Lengkapi semua test case hingga Line Coverage menyentuh 100%.
- Buat kode test case yang meaningful dan thorough. Cek apakah dependensi eksternal seperti repository atau payment gateway API dipanggil sesuai kebutuhan setiap test case.
- Integrasikan project Anda dengan SonarQube.
Contoh project SIBAYAR yang sudah memenuhi Line Coverage 100%:
Penutup
Kita sudah bersama-sama membuat unit test untuk Service, lengkap dengan cara menggunakan mock dan stub.
Untuk bahan diskusi saat refleksi:
- Apakah Line Coverage 100% menjamin tidak ada bug? Apakah Line Coverage 100% menjamin aspek FIRST principle terutama aspek Thorough?
- Bagaimana jika kita langsung melakukan modifikasi database atau mengakses langsung library atau API eksternal saat kita melakukan unit test? Apa dampaknya bagi konsistensi hasil dari unit test? Apa dampaknya bagi FIRST principle pada unit test yang dibuat, terutama aspek Fast, Isolated, dan Repeatable?
- Apa kesulitan yang dialami Bapak/Ibu ketika menjalani tutorial ini?
Untuk hari ketiga, kita akan mendalami mengenai Functional Test dan Behaviour-Driven Development (BDD).
Reflection Notes
Mengenai FIRST Principle
- Fast
- Proses testing harus dilakukan secara cepat, oleh karena itu testing
Servicedan komponen lain di Spring tidak menggunakanMockMvclayaknya diController. Hal ini karena diService, kita cukup fokus dengan logic kita yang sudah terpisah dari dependensi framework Spring Boot. - Mengapa kita harus melakukan mock dan stub untuk akses database?
Karena kita tidak ingin akses database memperlambat proses testing kita. Bahkan, kita sebisa mungkin menghindari alokasi resource tambahan untuk database terpisah hanya untuk unit test.
- Proses testing harus dilakukan secara cepat, oleh karena itu testing
- Isolated
- Mengapa kita harus melakukan mock dan stub untuk akses database, library, atau API eksternal?
Karena kita tidak ingin hasil dari proses eksekusi suatu unit test memengaruhi hasil unit test lain. Dengan melakukan mock dan stub terhadap dependensi eksternal, kita tidak mengotori data pada dependensi eksternal tersebut dengan dummy data dari unit test kita.
- Mengapa kita harus melakukan mock dan stub untuk akses database, library, atau API eksternal?
- Repeatable
- Aspek ini punya kaitan erat dengan aspek Isolated. Dengan unit test yang terisolasi, kita bisa mendapatkan hasil yang konsisten meskipun testing dilakukan berulang-ulang kali.
- Self-Validating
- Aspek ini sudah otomatis ter-cover dengan penggunaan testing library JUnit, yang juga telah kita gunakan di tutorial.
- Pastikan saja setiap unit test memiliki 3A:
- Arrange: Persiapkan semua data dan argumen untuk testing.
- Act: Jalankan fungsi yang akan di-test.
- Assert: Lakukan pengecekan terhadap hasil keluaran dari fungsi yang di-test. Ini juga termasuk jika fungsi tersebut mengeluarkan exception.
- Thorough
- Pastikan semua happy path dan edge case telah diuji.
- Lihat kembali Acceptance Criteria dari suatu use case untuk fungsi yang akan di-test.
Apakah Coverage 100% menjamin kode bersih dari bug?
Tidak bisa menjamin. Inilah pentingnya aspek Thorough pada FIRST Principle.
Kalaupun aspek Thorough pada unit test sudah terpenuhi, kita juga masih tidak bisa menjamin program bebas bug ketika proses integrasi. Oleh karena itu dibutuhkan integration test (yang akan dibahas lebih lanjut pada Day 3).
Contoh sederhananya adalah pada test untuk Controller di SIBAYAR.
Ternyata walaupun Code Coverage dan Branch Coverage-nya sudah 100%, kita belum melakukan test untuk otorisasi (endpoint X hanya bisa diakses oleh role A).
Proses test untuk otorisasi di Spring Boot tidak bisa dilakukan dengan unit test, namun dengan integration test.
Dalam integration test, kita akan menjalankan aplikasinya secara keseluruhan, yang tentunya akan membutuhkan waktu eksekusi lebih lama dibandingkan unit test.
Created: 2023-10-17 02:48:29