diff --git a/docs/workshops/day_2_tdd.md b/docs/workshops/day_2_tdd.md index 9526f8d7da1c93d8dee7ed76c3ead23ccce9740e..65f0a0e2f233f857bfb7f04cdc86505c19b10c28 100644 --- a/docs/workshops/day_2_tdd.md +++ b/docs/workshops/day_2_tdd.md @@ -1,9 +1,502 @@ # Day 2: Test-Driven Development -* Session 1: 9:00 - 10:00: [Testing Category][1] -* Session 2: 10:15 - 11:30: Hands-on: A bigger Case study -* Session 3: 13:30 - 15:00: Hands-on: A bigger Case study -* Session 4: 15:30 - 16:30: Overview, Discussion, Lesson learned +* Session 1: 9:00 - 10:00: [Testing Category][1] +* Session 2: 10:15 - 11:30: Hands-on: A bigger Case study +* Session 3: 13:30 - 15:00: Hands-on: A bigger Case study +* Session 4: 15:30 - 16:30: Overview, Discussion, Lesson learned -[1]: https://docs.google.com/presentation/d/1f1vpyYOu3GSyIeyqGLU-D6REBn0ACN81?rtpof=true&usp=drive_fs +## 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, sembari mengikuti *flow* Test-Driven Development: +- 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 mau melihat apakah fungsi kita melakukan proses *Save Object* ke sebuah database. +Berikut adalah contoh penggunaan *mock* di *test code*: +```java + @InjectMocks + PaymentService service; // Mock object dari PaymentRepository akan ditanam dalam object "service" + + @Mock + PaymentRepository repository; // Mock object dari PaymentRepository yang akan kita gunakan + + // Test ini bertujuan untuk melihat apakah method "save" pada PaymentRepository dieksekusi setidaknya 1 kali ketika method "create" di PaymentService dieksekusi. + @Test + void testSaveObjectExecutedWhenCreateExecuted() { + service.create("a-01", "Bambang", 20000); + verify(repository, atLeastOnce()).save(any(Payment.class)); + } +``` + +**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 mau untuk diproses oleh fungsi yang kita *test*. +Kita juga bisa meminta *stub* untuk memberikan *error* untuk menyimulasikan *negative case*. + +Contoh sederhananya adalah, kita mau 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*: +```java + @InjectMocks + PaymentService service; // Stub object dari PaymentRepository akan ditanam dalam object "service" + + @Mock + PaymentRepository repository; // Stub object dari PaymentRepository yang akan kita gunakan + + // Test ini bertujuan untuk melihat apakah "create" akan mengembalikan error jika payment dengan ID yang sama ("a-01") sudah ada. + @Test + void testCreatePaymentReturnsErrorIfPaymentAlreadyExists() { + // Proses stubbing: jika repository.getById("a-01") dieksekusi, kembalikan objek Payment yang sudah dibuat + Payment payment = new Payment("a-01", "Bambang", 20000); + when(repository.getById("a-01")).thenReturn(payment); + + // Cek apakakh eksekusi method "create" akan memunculkan exception PaymentAlreadyCreated + assertThrows(PaymentAlreadyCreated.class, () -> { + service.create("a-01", "Usep", 100000); + }); + } +``` + + +## Persiapan Workshop + +Untuk mengikuti rangkaian kegiatan workshop hari ini, harap persiapkan _tools_ berikut di komputer anda: + +- Git +- IntelliJ Community Edition +- Java JDK 17 (`java` dan `javac`) +- Apache Maven (`mvn`) + +Buat salinan _branch_ `main` dari repositori Git kode templat workshop hari ini: + +```shell +git clone https://gitlab.cs.ui.ac.id/pmpl/workshops/sibayar.git +``` + +Apabila sudah menyalin repositori Git dan telah menyiapkan _tools_ yang dibutuhkan, +Anda bisa menjalanakan aplikasi contoh dengan perintah *shell* berikut: + +```shell +mvn package -DskipTests +cp src/main/resources/application.properties target/application.properties +cd target +java -jar sibayar-0.1.1-SNAPSHOT.jar +``` + +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: +1. User bisa melakukan transfer uang ke User lain, atau ke nomor rekening yang tidak terdaftar di SIBAYAR. +2. SIBAYAR mengontak API Flip untuk mendapatkan link pembayaran, + yang bisa digunakan User untuk membayar transfer tersebut. +3. User melakukan pembayaran menggunakan link pembayaran yang telah diberikan API Flip ke User melalui SIBAYAR. +4. Jika user sudah selesai melakukan pembayaran, + API Flip akan mengeksekusi *payment callback endpoint* milik SIBAYAR untuk melanjutkan proses transfer. +5. Ketika API Flip mengeksekusi *payment callback endpoint* SIBAYAR, + sistem SIBAYAR akan kembali mengontak API Flip untuk transfer uang (*disbursement*) ke rekening tujuan. +6. 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*: +1. `AuthenticationController`: untuk proses *login* dan *register* akun baru. +2. `PaymentController`: untuk melakukan *payment* baru dan melihat histori *payment*. +3. `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 yang akan dijalankan sebagai *set up* setiap *unit test* pada *test suite*. +Fungsi `setUp()` akan dijalankan **sebelum** setiap *unit test* dijalankan. +Dengan fungsi `setUp()`, kita bisa mengisolasi 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 kelas `PaymentServiceImpl`. +Buat sejajar dengan kelas aslinya, yaitu di *package* `com.example.sibayar.service.payment`: +```java +@ExtendWith(MockitoExtension.class) +class PaymentServiceImplTest { + + @InjectMocks + private PaymentServiceImpl service; + @Mock + private PaymentRepository paymentRepository; + @Mock + private UserRepository userRepository; + @Mock + User user; + + Payment payment; + PaymentToUserRequest paymentToUserRequest; + PaymentToOtherRequest paymentToOtherRequest; + PaymentLinkResponse paymentLinkResponse; + + // Instansiasi objek-objek yang akan menjadi hasil dari stub + @BeforeEach + void setUp() { + paymentToOtherRequest = PaymentToOtherRequest.builder() + .destinationName("Pak Bambang") + .destinationBankCode("bca") + .destinationAccountNumber("123456789") + .amount(50000) + .build(); + paymentToUserRequest = PaymentToUserRequest.builder() + .destinationUserId(2) + .amount(50000) + .build(); + paymentLinkResponse = PaymentLinkResponse.builder() + .expiredDate(LocalDateTime.of(2023, 11, 4, 23, 59)) + .linkId(1) + .linkUrl("https://flip.id/bambang") + .build(); + payment = Payment.builder() + .id(1) + .sender(user) + .destinationName("Pak Bambang") + .destinationBankCode("bca") + .destinationAccountNumber("123456789") + .amount(50000) + .paymentLinkId(1) + .paymentLinkUrl("https://flip.id/bambang") + .expiredDate(LocalDateTime.of(2023, 11, 4, 23, 59)) + .status(String.valueOf(PaymentStatus.WAITING_PAYMENT)) + .build(); + } +} +``` + +Dalam kasus ini, terdapat 3 objek yang akan dibuat objek *mock*/*stub*-nya, yang ditandai dengan anotasi `@Mock`: +1. `PaymentRepository`: repositori untuk Payment, diperlukan untuk operasi *database* terkait entri `Payment`. +2. `UserRepository`L repositori untuk Payment, diperlukan untuk operasi *database* terkait entri `User`. +3. `User`: objek data User (pengguna). + +Ketiga objek ini kemudian akan di-*inject* ke `PaymentServiceImpl` yang akan kita test, ditandai dengan anotasi `@InjectMocks`. + +Terdapapt juga 2 objek yang akan menjadi parameter dari *fungsi* yang akan di-*test*, yaitu: +1. `PaymentToUserRequest`: *Data Transfer Object* (DTO) sebagai *parameter* fungsi `payToUser`, yang datang dari `PaymentController`. +2. `PaymentToOtherRequest`: *Data Transfer Object* (DTO) sebagai *parameter* fungsi `payToOtherDestination`, yang datang dari `PaymentController`. + +Terdapat juga 2 objek yang akan menjadi hasil dari *stub*, yaitu: +1. `Payment`: objek data Payment. +2. `PaymentLinkResponse`: *Data Transfer Object* (DTO) untuk menampung hasil kembalian dari Flip API ketika membuat *payment link*. + +### Tugas Anda +- [ ] Silakan *copy* snippet kode yang telah dijelaskan ke dalam project Anda. + +### Latihan Mandiri: Inisiasi *test suite* `CallbackServiceImpl` +- [ ] Buat *test suite* `CallbackServiceImpl`, sejajar dengan *test suite* `PaymentServiceImpl` yang telah dibuat. +- [ ] *Test suite* terdiri dari objek *mock/stub* berikut: + 1. `PaymentRepositoryImpl` +- [ ] *Test suite* terdiri dari objek untuk parameter fungsi yang di-*test* berikut: + 1. `PaymentCallbackRequest` + 2. `DisbursementCallbackRequest` +- [ ] *Test suite* terdiri dari objek *hasil stub* berikut: + 1. `DisbursementResponse` +- [ ] Buat fungsi `setUp()` untuk menyusun objek-objek *hasil stub*. + + +## Melakukan *Mock* dan *Stub* untuk Akses Database dan Helper Function Lain + +SIBAYAR menggunakan Spring Data JPA untuk mengakases 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 objek `Payment` dan objek `User`. + +*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: + +### `payToUser` sukses menyimpan dan mengembalikan objek `Payment` baru +```java + @Test + void testPayToUserSuccess() { + when(userRepository.findById(any(Integer.class))).thenReturn(Optional.of(user)); + when(service.getPaymentLink(any(PaymentLinkRequest.class))).thenReturn(paymentLinkResponse); + + Payment result = service.payToUser(1, paymentToUserRequest); + + verify(service, atLeastOnce()).getPaymentLink(any(PaymentLinkRequest.class)); + verify(paymentRepository, atLeastOnce()).save(result); + + assertEquals(paymentLinkResponse.getExpiredDate(), result.getExpiredDate()); + assertEquals(paymentLinkResponse.getLinkId(), result.getPaymentLinkId()); + assertEquals(paymentLinkResponse.getLinkUrl(), result.getPaymentLink()); + } +``` + +Terlihat bahwa terdapat *stubbing* untuk fungsi `userRepository.findById(Integer)` untuk menyimulasikan bahwa di database terdapat objek *mock* `user`. +Selain itu, juga terdapat *stubbing* untuk fungsi `service.getPaymentLink(PaymentLinkRequest)` untuk mengembalikan objek `paymentLinkResponse`. +*Test case* ini akan mengecek: +1. Apakah fungsi `payToUser` memanggil *helper function* `getPaymentLink` untuk akses API Flip? +2. Apakah fungsi `payToUser` memanggil fungsi `save` pada `PaymentRepository` untuk menyimpan objek `Payment` ke dalam database? +3. Apakah fungsi `payToUser` mengembalikan objek `Payment`, dengan isi yang sama seperti yang dikembalikan oleh API Flip? + +### `paymentCallback` sukses menyimpan status `WAITING_DISBURSEMENT` ketika status Payment Link `"SUCCESSFUL"` dan status Disbursement `"PENDING"` +```java + @Test + void testPaymentCallbackWithSuccessfulStatusAndPendingDisbursement() { + when(paymentRepository.findByPaymentLinkId(any(Integer.class))).thenReturn(Optional.of(payment)); + paymentCallbackRequest.setStatus("SUCCESSFUL"); + when(service.disburseMoney(any(DisbursementRequest.class))).thenReturn(disbursementResponse); + disbursementResponse.setStatus("PENDING"); + + Payment payment = service.paymentCallback(paymentCallbackRequest); + + verify(service, atLeastOnce()).disburseMoney(any(DisbursementRequest.class)); + verify(paymentRepository, atLeastOnce()).save(result); + + assertEquals(PaymentStatus.WAITING_DISBURSEMENT.getStatus(), payment.getStatus()); + } +``` + +Fungsi `paymentCallback` bertujuan untuk meneruskan pembayaran lewat mekanisme *Disbursement* jika status Payment Link yang diberikan `"SUCCESSFUL"`. + +Terlihat bahwa terdapat *stubbing* untuk fungsi `paymentRepository.findByPaymentLinkId(Integer)` untuk menyimulasikan bahwa di database terdapat objek `payment`. +Selain itu, juga terdapat *stubbing* untuk fungsi `service.disburseMoney(DisbursementRequest)` untuk mengembalikan objek `paymentLinkResponse`. +Selain itu, kita juga perlu *set status* pada objek `paymentCallbackRequest` menjadi `"SUCCESSFUL"`. +Selain itu, kita juga perlu *set status* pada objek `disbursementResponse` menjadi `"PENDING"` untuk menyimulasikan API Flip sedang melakukan proses *disbursement*. +*Test case* ini akan mengecek: +1. Apakah fungsi `paymentCallback` memanggil *helper function* `disburseMoney` untuk akses API Flip? +2. Apakah fungsi `paymentCallback` memanggil fungsi `save` pada `PaymentRepository` untuk menyimpan perubahan objek `Payment` ke dalam database? +3. Apakah fungsi `paymentCallback` mengembalikan objek `Payment` dengan `status` berupa `"WAITING_DISBURSEMENT"`? + +### `paymentCallback` sukses menyimpan status `WAITING_DISBURSEMENT` ketika status Payment Link `"SUCCESSFUL"` dan status Disbursement `"DONE"` +```java + @Test + void testPaymentCallbackWithSuccessfulStatusAndSuccessfulDisbursement() { + when(paymentRepository.findByPaymentLinkId(any(Integer.class))).thenReturn(Optional.of(payment)); + paymentCallbackRequest.setStatus("SUCCESSFUL"); + when(service.disburseMoney(any(DisbursementRequest.class))).thenReturn(disbursementResponse); + disbursementResponse.setStatus("DONE"); + + Payment payment = service.paymentCallback(paymentCallbackRequest); + + verify(service, atLeastOnce()).disburseMoney(any(DisbursementRequest.class)); + verify(paymentRepository, atLeastOnce()).save(result); + + assertEquals(PaymentStatus.SUCCESS.getStatus(), payment.getStatus()); + } +``` + +Terlihat bahwa terdapat *stubbing* untuk fungsi `paymentRepository.findByPaymentLinkId(Integer)` untuk menyimulasikan bahwa di database terdapat objek `payment`. +Selain itu, juga terdapat *stubbing* untuk fungsi `service.disburseMoney(DisbursementRequest)` untuk mengembalikan objek `paymentLinkResponse`. +Selain itu, kita juga perlu *set status* pada objek `paymentCallbackRequest` menjadi `"SUCCESSFUL"`. +Selain itu, kita juga perlu *set status* pada objek `disbursementResponse` menjadi `"DONE"` untuk menyimulasikan API Flip langsung selesai melakukan *disbursement*. +*Test case* ini akan mengecek: +1. Apakah fungsi `paymentCallback` memanggil *helper function* `disburseMoney` untuk akses API Flip? +2. Apakah fungsi `paymentCallback` memanggil fungsi `save` pada `PaymentRepository` untuk menyimpan perubahan objek `Payment` ke dalam database? +3. Apakah fungsi `paymentCallback` mengembalikan objek `Payment` dengan `status` berupa `"SUCCESS"`? + +### Tugas Anda +- [ ] Silakan *copy* semua snippet kode yang telah dijelaskan ke dalam project Anda ke tempat yang sesuai. + - `testPayToUserSuccess` diletakkan di `PaymentServiceImplTest`. + - `testPaymentCallbackWithSuccessfulStatusAndPendingDisbursement` diletakkan di `CallbackServiceImplTest`. + - `testPaymentCallbackWithSuccessfulStatusAndSuccessfulDisbursement` diletakkan di `CallbackServiceImplTest`. + +### Latihan Mandiri: Buat *positive case* +- [ ] Buat *test case* `testPayToOtherSuccess` pada *test suite* `PaymentServiceImplTest` untuk mengecek: + 1. Apakah fungsi `payToOtherDestination` memanggil *helper function* `getPaymentLink` untuk akses API Flip? + 2. Apakah fungsi `payToOtherDestination` memanggil fungsi `save` pada `PaymentRepository` untuk menyimpan objek `Payment` ke dalam database? + 3. Apakah fungsi `payToOtherDestination` mengembalikan objek `Payment`, dengan isi yang sama seperti yang dikembalikan oleh API Flip? + +### Latihan Mandiri: Buat *negative case* +- [ ] Buat *test case* `testPaymentCallbackWithSuccessfulStatusButCancelledDisbursement` pada *test suite* `CallbackServiceImplTest` untuk mengecek: + 1. Apakah fungsi `paymentCallback` memanggil *helper function* `getPaymentLink` untuk akses API Flip? + 2. Apakah fungsi `paymentCallback` memanggil fungsi `save` pada `PaymentRepository` untuk menyimpan perubahan objek `Payment` ke dalam database? + 3. Apakah fungsi `paymentCallback` mengembalikan objek `Payment` dengan `status` berupa `"DISBURSEMENT_FAILED"`? + - **CATATAN**: `status` pada `disbursementResponse` harus diganti dengan `"CANCELLED"`. +- [ ] Buat *test case* `testPaymentCallbackWithCancelledStatus` pada *test suite* `CallbackServiceImplTest` untuk mengecek: + 1. Apakah fungsi `paymentCallback` **TIDAK** memanggil *helper function* `getPaymentLink` untuk akses API Flip?<br /> + **HINT**: gunakan + ```java + verify(service, atMost(0)).disburseMoney(any(DisbursementRequest.class)) + ``` + untuk mengecek bahwa *helper function* tidak dipanggil. + 2. Apakah fungsi `paymentCallback` memanggil fungsi `save` pada `PaymentRepository` untuk menyimpan perubahan objek `Payment` ke dalam database? + 3. Apakah fungsi `paymentCallback` mengembalikan objek `Payment` dengan `status` berupa `"PAYMENT_FAILED"`? + - **CATATAN**: `status` pada `paymentCallbackRequest` harus diganti dengan `"CANCELLED"`. + + +## 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*). +Sebagai contoh, berikut adalah beberapa *negative case* yang bisa Anda gunakan: + +### `payToUser` pada `PaymentServiceImpl` akan mengembalikan *error* ketika user ID dari *sender* dan *destination* sama. +```java + @Test + void testPayToUserFailWhenSenderAndDestinationAreTheSame() { + assertThrows(SelfPaymentException.class, () -> { + service.payToUser(paymentToUserRequest.getDestinationUserId(), paymentToUserRequest); + }); + } +``` + +*Test case* ini akan mengecek apakah *exception* `SelfPaymentException` dikeluarkan jika user ID dari *sender* dan *destination* sama. + +### `payToUser` pada `PaymentServiceImpl` akan mengembalikan *error* jika Flip API sedang tidak bisa diakses. +```java + @Test + void testPayToUserThrowsExceptionWhenAPIIsUnreachable() { + when(userRepository.findById(any(Integer.class))).thenReturn(Optional.of(user)); + when(service.getPaymentLink(any(PaymentLinkRequest.class))).thenThrow(new APIUnreachableException("flip")); + + assertThrows(APIUnreachableException.class, () -> { + service.payToUser(1, paymentToUserRequest); + }); + verify(service, atLeastOnce()).getPaymentLink(any(PaymentLinkRequest.class)); + } +``` + +Terlihat bahwa terdapat *stubbing* untuk fungsi `userRepository.findById(Integer)` untuk mengembalikan objek *mock* `user`. +Selain itu, juga terdapat *stubbing* untuk fungsi `service.getPaymentLink(PaymentLinkRequest)` untuk melempar *exception* `APIUnreachableException`. +*Test case* ini akan mengecek: +1. Apakah fungsi `payToUser` meneruskan *exception* `APIUnreachableException`? +2. Apakah fungsi `payToUser` memanggil *helper function* `getPaymentLink` untuk akses API Flip? + +### `paymentCallback` pada `CallbackServiceImpl` akan mengembalikan *error* jika pembayaran lewat payment link sukses, namun Flip API untuk *disbursement* sedang tidak bisa diakses. +```java + @Test + void testPaymentCallbackWithSuccessfulStatusThrowsExceptionWhenAPIIsUnreachable() { + when(paymentRepository.findByPaymentLinkId(any(Integer.class))).thenReturn(Optional.of(payment)); + paymentCallbackRequest.setStatus("SUCCESSFUL"); + when(service.disburseMoney(any(DisbursementRequest.class))).thenThrow(new APIUnreachableException("flip")); + + assertThrows(APIUnreachableException.class, () -> { + service.paymentCallback(paymentCallbackRequest); + }); + verify(service, atLeastOnce()).disburseMoney(any(DisbursementRequest.class)); + } +``` + +Terlihat bahwa terdapat *stubbing* untuk fungsi `paymentRepository.findByPaymentLinkId(Integer)` untuk mengembalikan objek `payment`. +Selain itu, juga terdapat *stubbing* untuk fungsi `service.disburseMoney(DisbursementRequest)` untuk melempar *exception* `APIUnreachableException`. +Selain itu, kita juga perlu *set status* pada objek `paymentCallbackRequest` menjadi `"SUCCESSFUL"`, +karena akses API Flip untuk *disbursement* hanya akan diakses jika status *payment link* sudah `SUCCESSFUL`. +*Test case* ini akan mengecek: +1. Apakah fungsi `payToUser` meneruskan *exception* `APIUnreachableException`? +2. Apakah fungsi `payToUser` memanggil *helper function* `disburseMoney` untuk akses API Flip? + +### Tugas Anda +- [ ] Silakan *copy* semua snippet kode yang telah dijelaskan ke dalam project Anda ke tempat yang sesuai. + - `testPayToUserFailWhenSenderAndDestinationAreTheSame` diletakkan di `PaymentServiceImplTest`. + - `testPayToUserThrowsExceptionWhenAPIIsUnreachable` diletakkan di `PaymentServiceImplTest`. + - `testPaymentCallbackWithSuccessfulStatusThrowsExceptionWhenAPIIsUnreachable` diletakkan di `CallbackServiceImplTest`. + + +### Latihan Mandiri: Buat *negative case* +- [ ] Buat *test case* `testPayToOtherThrowsExceptionWhenAPIIsUnreachable` pada *test suite* `PaymentServiceImplTest` untuk mengecek: + 1. Apakah fungsi `payToOtherDestination` meneruskan *exception* `APIUnreachableException`? + 2. Apakah fungsi `payToOtherDestination` menjalankan *helper function* `getPaymentLink` untuk akses API Flip? + + +## *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? + +### Tugas Anda +- [ ] Buat interface baru `PaymentGatewayAPI`, dengan menggunakan *snippet* berikut: + ```java + public interface PaymentGatewayAPI { + PaymentLinkResponse getPaymentLink(PaymentLinkRequest request); + DisbursementResponse disburseMoney(DisbursementRequest request); + } + ``` +- [ ] Buat class baru `FlipAPI`, dengan menggunakan *snippet* berikut: + ```java + @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()); + } + } + ``` +- [ ] Pada `PaymentServiceImpl` dan `CallbackServiceImpl`, buat koneksi baru ke `PaymentGatewayAPI` dengan menggunakan *snippet* berikut: + ```java + @Qualifier("flipAPI") + private final PaymentGatewayAPI api; + ``` +- [ ] Gunakan `api` untuk mengakses fungsi `getPaymentLink` dan `disburseMoney` yang telah dipindahkan. +- [ ] Tambahkan objek *mock* untuk `PaymentGatewayAPI` pada *test suite* `PaymentServiceImplTest` dan `CallbackServiceImplTest` dengan menggunakan *snippet* berikut: + ```java + @Mock + private PaymentGatewayAPI flipAPI; + ``` +- [ ] Gunakan objek *mock* `flipAPI` untuk menggantikan pemanggilan fungsi `getPaymentLink` dan `disburseMoney` pada setiap `test case`. Misal: + ```java + when(service.disburseMoney(any(DisbursementRequest.class))).thenReturn(disbursementResponse); + // ... + verify(service, atLeastOnce()).disburseMoney(any(DisbursementRequest.class)); + ``` + menjadi seperti berikut: + ```java + 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*. + +## 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? +- [ ] 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 kesulitan yang dialami Bapak/Ibu ketika menjalani tutorial ini? + + + +[1]: https://docs.google.com/presentation/d/1f1vpyYOu3GSyIeyqGLU-D6REBn0ACN81?rtpof=true&usp=drive_fs