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 (
java
danjavac
) - 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
PaymentServiceImplTest
sesuai dengan arahan yang diberikan.- Test suite terdiri dari objek mock/stub berikut:
PaymentRepository
UserRepository
User
- Test suite terdiri dari objek untuk parameter fungsi yang di-test berikut:
PaymentToUserRequest
PaymentToOtherRequest
- Test suite terdiri dari objek hasil stub berikut:
Payment
PaymentLinkResponse
- 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 suitePaymentServiceImplTest
yang telah dibuat.- Test suite terdiri dari objek mock/stub berikut:
PaymentRepository
- Test suite terdiri dari objek untuk parameter fungsi yang di-test berikut:
PaymentCallbackRequest
DisbursementCallbackRequest
- Test suite terdiri dari objek hasil stub berikut:
Payment
DisbursementResponse
- 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
payToUser
memanggil setidaknya satu kali helper functiongetPaymentLink
untuk akses API Flip? (sintaks verifikasi pemanggilan mock ada di line 8) - Apakah fungsi
payToUser
memanggil setidaknya satu kali fungsisave
padaPaymentRepository
untuk menyimpan objekPayment
ke dalam database? (sintaks verifikasi pemanggilan mock ada di line 9) - Apakah fungsi
payToUser
mengembalikan 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
paymentCallbackRequest
menjadi"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
disbursementResponse
menjadi"PENDING"
untuk menyimulasikan API Flip sedang melakukan proses disbursement.
Test case ini akan mengecek:
- Apakah fungsi
paymentCallback
memanggil setidaknya satu kali helper functiondisburseMoney
untuk akses API Flip? (sintaks verifikasi pemanggilan mock ada di line 10) - Apakah fungsi
paymentCallback
memanggil setidaknya satu kali fungsisave
padaPaymentRepository
untuk menyimpan perubahan objekPayment
ke dalam database? (sintaks verifikasi pemanggilan mock ada di line 11) - Apakah fungsi
paymentCallback
mengembalikan objekPayment
denganstatus
berupa"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
paymentCallbackRequest
menjadi"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
disbursementResponse
menjadi"DONE"
, untuk menyimulasikan API Flip langsung selesai melakukan disbursement.
Test case ini akan mengecek:
- Apakah fungsi
paymentCallback
memanggil setidaknya satu kali helper functiondisburseMoney
untuk akses API Flip? (sintaks verifikasi pemanggilan mock ada di line 10) - Apakah fungsi
paymentCallback
memanggil setidaknya satu kali fungsisave
padaPaymentRepository
untuk menyimpan perubahan objekPayment
ke dalam database? (sintaks verifikasi pemanggilan mock ada di line 11) - Apakah fungsi
paymentCallback
mengembalikan objekPayment
denganstatus
berupa"SUCCESS"
?
Tugas Anda
- Buat test case
testPayToUserSuccess
sesuai 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
testPaymentCallbackWithSuccessfulStatusAndPendingDisbursement
sesuai 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
testPaymentCallbackWithSuccessfulStatusAndSuccessfulDisbursement
sesuai 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
testPayToOtherSuccess
pada test suitePaymentServiceImplTest
untuk mengecek:- Apakah fungsi
payToOtherDestination
memanggil helper functiongetPaymentLink
untuk akses API Flip? - Apakah fungsi
payToOtherDestination
memanggil fungsisave
padaPaymentRepository
untuk menyimpan objekPayment
ke dalam database? - Apakah fungsi
payToOtherDestination
mengembalikan objekPayment
, dengan isi yang sama seperti yang dikembalikan oleh API Flip?
- Apakah fungsi
Latihan Mandiri: Buat beberapa negative case
- Buat test case
testPaymentCallbackWithSuccessfulStatusButCancelledDisbursement
pada test suiteCallbackServiceImplTest
untuk mengecek:- Apakah fungsi
paymentCallback
memanggil setidaknya satu kali helper functiondisburseMoney
untuk akses API Flip? - Apakah fungsi
paymentCallback
memanggil setidaknya satu kali fungsisave
padaPaymentRepository
untuk menyimpan perubahan objekPayment
ke dalam database? - Apakah fungsi
paymentCallback
mengembalikan objekPayment
denganstatus
berupa"DISBURSEMENT_FAILED"
? - CATATAN:
status
padadisbursementResponse
harus diganti dengan"CANCELLED"
.
- Apakah fungsi
- Buat test case
testPaymentCallbackWithCancelledStatus
pada test suiteCallbackServiceImplTest
untuk mengecek:- Apakah fungsi
paymentCallback
TIDAK memanggil helper functiondisburseMoney
untuk akses API Flip?
HINT: gunakanuntuk mengecek bahwa helper function tidak dipanggil.1
verify(service, atMost(0)).disburseMoney(any(DisbursementRequest.class))
- Apakah fungsi
paymentCallback
memanggil setidaknya satu kali fungsisave
padaPaymentRepository
untuk menyimpan perubahan objekPayment
ke dalam database? - Apakah fungsi
paymentCallback
mengembalikan objekPayment
denganstatus
berupa"PAYMENT_FAILED"
? - CATATAN:
status
padapaymentCallbackRequest
harus 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
payToUser
meneruskan exceptionAPIUnreachableException
? (pengecekan di line 6) - Apakah fungsi
payToUser
memanggil helper functiongetPaymentLink
untuk 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
paymentCallbackRequest
menjadi"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
payToUser
meneruskan exceptionAPIUnreachableException
? (pengecekan di line 8) - Apakah fungsi
payToUser
memanggil helper functiondisburseMoney
untuk akses API Flip? (sintaks verifikasi pemanggilan mock ada di line 11)
Tugas Anda
- Buat test case
testPayToUserFailWhenSenderAndDestinationAreTheSame
sesuai 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
testPayToUserThrowsExceptionWhenAPIIsUnreachable
sesuai 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
testPaymentCallbackWithSuccessfulStatusThrowsExceptionWhenAPIIsUnreachable
sesuai 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
testPayToOtherThrowsExceptionWhenAPIIsUnreachable
pada test suitePaymentServiceImplTest
untuk mengecek:- Apakah fungsi
payToOtherDestination
meneruskan exceptionAPIUnreachableException
? - Apakah fungsi
payToOtherDestination
memanggil setidaknya satu kali helper functiongetPaymentLink
untuk 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
PaymentGatewayAPI
di 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); }
PaymentGatewayAPI
adalah untuk memastikan bahwa semua class penghubung dengan API payment gateway dapat diakses dengan cara yang sama. -
Buat class baru
FlipAPI
di 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
getPaymentLink
yang sebelumnya ada dicom.example.sibayar.service.payment.PaymentServiceImpl
, ke dalam methodgetPaymentLink
yang ada diFlipAPI
. - Pindahkan juga isi method
disburseMoney
yang sebelumnya ada dicom.example.sibayar.service.payment.CallbackServiceImpl
, ke dalam methodgetPaymentLink
yang ada diFlipAPI
.
- Pindahkan juga isi method
-
Pada
PaymentServiceImpl
danCallbackServiceImpl
, buat koneksi baru ke implementasi dariPaymentGatewayAPI
dengan cara membuat instance variable baru. Gunakan snippet berikut untuk diletakkan di definisi class dariPaymentServiceImpl
danCallbackServiceImpl
.1 2
@Qualifier("flipAPI") private final PaymentGatewayAPI api;
@Qualifier("flipAPI")
akan otomatis melakukan dependency injection sehingga variabelapi
akan berisi objek dariFlipAPI
. - Gunakan
api
untuk mengakses fungsigetPaymentLink
dandisburseMoney
yang telah dipindahkan keFlipAPI
. Misal dari sebelumnya:menjadi seprti berikut:1
PaymentLinkResponse apiResponse = getPaymentLink(apiRequest);
1
PaymentLinkResponse apiResponse = api.getPaymentLink(apiRequest);
- Tambahkan objek mock untuk
PaymentGatewayAPI
pada test suitePaymentServiceImplTest
danCallbackServiceImplTest
dengan menggunakan snippet berikut:1 2
@Mock private PaymentGatewayAPI flipAPI;
- Gunakan objek mock
flipAPI
untuk menggantikan pemanggilan fungsigetPaymentLink
dandisburseMoney
pada 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
Service
dan komponen lain di Spring tidak menggunakanMockMvc
layaknya 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