diff --git a/docs/workshops/day_2_tdd.md b/docs/workshops/day_2_tdd.md index 65f0a0e2f233f857bfb7f04cdc86505c19b10c28..85982839746c4e05e7c4c2ac66811856404b7d4b 100644 --- a/docs/workshops/day_2_tdd.md +++ b/docs/workshops/day_2_tdd.md @@ -1,22 +1,23 @@ # 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 1: 9:00 - 10:30: [Testing Category][1] +* Session 2: 10:30 - 11:15: Hands-on: A bigger Case study +* Session 3: 13:00 - 15:30: 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, 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. +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*?** @@ -24,7 +25,7 @@ Kita akan memanfaatkan teknik berupa *mock* dan *stub* untuk membuat kode cakupa *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. +Contoh sederhananya adalah, kita ingin melihat apakah fungsi kita melakukan proses *Save Object* ke sebuah database. Berikut adalah contoh penggunaan *mock* di *test code*: ```java @InjectMocks @@ -46,10 +47,10 @@ Berikut adalah contoh penggunaan *mock* di *test code*: *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 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 mau menyimulasikan bahwa terdapat objek *Payment* di database, +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*: ```java @@ -66,7 +67,7 @@ Berikut adalah contoh penggunaan *stub* di *test code*: Payment payment = new Payment("a-01", "Bambang", 20000); when(repository.getById("a-01")).thenReturn(payment); - // Cek apakakh eksekusi method "create" akan memunculkan exception PaymentAlreadyCreated + // Cek apakah eksekusi method "create" akan memunculkan exception PaymentAlreadyCreated assertThrows(PaymentAlreadyCreated.class, () -> { service.create("a-01", "Usep", 100000); }); @@ -78,10 +79,10 @@ Berikut adalah contoh penggunaan *stub* di *test code*: 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`) +- [Git](https://git-scm.com/) +- [Java JDK 17](https://adoptium.net/temurin/releases/?version=17) (`java` dan `javac`) +- [IntelliJ IDEA Community Edition](https://www.jetbrains.com/idea/download/) +- [Apache Maven](https://maven.apache.org/download.cgi) (`mvn`) Buat salinan _branch_ `main` dari repositori Git kode templat workshop hari ini: @@ -104,26 +105,28 @@ 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. +SIBAYAR merupakan sistem pembayaran peer-to-peer sederhana yang menggunakan sebuah API *payment gateway* (dalam kasus ini, [Flip][2]) 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, +2. SIBAYAR mengontak API [Flip][2] 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. +3. User melakukan pembayaran menggunakan link pembayaran yang telah diberikan API [Flip][2] 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, + API [Flip][2] akan mengeksekusi *payment callback endpoint* milik SIBAYAR untuk melanjutkan proses transfer. +5. Ketika API [Flip][2] 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*, +6. Ketika [Flip][2] 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. +Untuk tutorial hari ini, tidak perlu khawatirkan akses *callback* dari [Flip][2], +karena [Flip][2] 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. +3. `CallbackController`: untuk *payment callback endpoint* dan *disbursement callback endpoint* yang akan dikontak oleh API [Flip][2]. ## Membuat *Test Suite Class* @@ -132,14 +135,18 @@ Untuk proses pembuatan *unit test*, kita akan memanfaatkan library `Mockito`. 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, 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 kelas `PaymentServiceImpl`. -Buat sejajar dengan kelas aslinya, yaitu di *package* `com.example.sibayar.service.payment`: +Berikut adalah contoh inisiasi *test suite* untuk *class* `PaymentServiceImpl`, yaitu *class* `PaymentServiceImplTest` yang dibuat di *folder* `src/test/com/example/sibayar/service/payment`: + ```java +package com.example.sibayar.service.payment; + +// ... import dependensi yang dibutuhkan untuk test + @ExtendWith(MockitoExtension.class) class PaymentServiceImplTest { @@ -161,73 +168,88 @@ class PaymentServiceImplTest { @BeforeEach void setUp() { paymentToOtherRequest = PaymentToOtherRequest.builder() - .destinationName("Pak Bambang") - .destinationBankCode("bca") - .destinationAccountNumber("123456789") - .amount(50000) - .build(); + .destinationName("Pak Bambang") + .destinationBankCode("bca") + .destinationAccountNumber("123456789") + .amount(50000) + .build(); paymentToUserRequest = PaymentToUserRequest.builder() - .destinationUserId(2) - .amount(50000) - .build(); + .destinationUserId(2) + .amount(50000) + .build(); paymentLinkResponse = PaymentLinkResponse.builder() - .expiredDate(LocalDateTime.of(2023, 11, 4, 23, 59)) - .linkId(1) - .linkUrl("https://flip.id/bambang") - .build(); + .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(); + .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). + +1. `PaymentRepository`: sebuah *class* yang bertugas untuk melakukan operasi *database* terhadap tabel `Payment`. Di Spring Boot, *class* ini disebut sebagai *repository*. +2. `UserRepository`: sebuah *class* yang bertugas untuk melakukan operasi *database* terhadap tabel `User`. Di Spring Boot, *class* ini disebut sebagai *repository*. +3. `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`. -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 parameter dari *fungsi* yang akan di-*test*, yaitu: + +1. `PaymentToUserRequest`: sebuah *Data Transfer Object* (DTO) sebagai argumen dari fungsi `payToUser`, yang datang dari `PaymentController`. +2. `PaymentToOtherRequest`: sebuah *Data Transfer Object* (DTO) sebagai argumen dari 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. +1. `Payment`: sebuah objek *database model* untuk tabel Payment. +2. `PaymentLinkResponse`: sebuah objek *Data Transfer Object* (DTO) untuk menampung hasil kembalian dari Flip API ketika membuat *payment link*. -### 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*. +### 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*. + +### Latihan Mandiri: Inisiasi *test suite* `CallbackServiceImplTest` +- [ ] Buat *test suite* `CallbackServiceImplTest`, sejajar dengan *test suite* `PaymentServiceImplTest` 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*. ## Melakukan *Mock* dan *Stub* untuk Akses Database dan Helper Function Lain -SIBAYAR menggunakan Spring Data JPA untuk mengakases database In-Memory H2. +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 objek `Payment` dan objek `User`. +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. @@ -235,7 +257,7 @@ Akan tetapi, hal tersebut jadi memakan *resource* lebih dengan perlu adanya *tes 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 +### Contoh 1: Fungsi `payToUser` sukses menyimpan dan mengembalikan objek `Payment` baru ```java @Test void testPayToUserSuccess() { @@ -253,14 +275,18 @@ Sebagai contoh, berikut adalah beberapa *test case* yang bisa Anda gunakan: } ``` -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`. +Berikut adalah penjelasan terkait *test case* ini: + +1. Di line 3, terdapat *stubbing* untuk fungsi `userRepository.findById(Integer)` untuk menyimulasikan bahwa di database terdapat objek *mock* `user`. +2. Di line 4, terdapat *stubbing* untuk fungsi `service.getPaymentLink(PaymentLinkRequest)` untuk mengembalikan objek `paymentLinkResponse`, untuk menyimulasikan kembalian dari pemanggilan API Flip untuk mendapatkan *payment link*. + *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? + +1. Apakah fungsi `payToUser` memanggil **setidaknya satu kali** *helper function* `getPaymentLink` untuk akses API Flip? (sintaks verifikasi pemanggilan *mock* ada di line 8) +2. Apakah fungsi `payToUser` memanggil **setidaknya satu kali** fungsi `save` pada `PaymentRepository` untuk menyimpan objek `Payment` ke dalam database? (sintaks verifikasi pemanggilan *mock* ada di line 9) 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"` +### Contoh 2: Fungsi `paymentCallback` sukses menyimpan status `WAITING_DISBURSEMENT` ketika status Payment Link `"SUCCESSFUL"` dan status Disbursement `"PENDING"` ```java @Test void testPaymentCallbackWithSuccessfulStatusAndPendingDisbursement() { @@ -280,16 +306,20 @@ Selain itu, juga terdapat *stubbing* untuk fungsi `service.getPaymentLink(Paymen 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*. +Berikut adalah penjelasan terkait *test case* ini: + +1. Di line 3, terdapat *stubbing* untuk fungsi `paymentRepository.findByPaymentLinkId(Integer)` untuk menyimulasikan bahwa di database terdapat objek `payment`. +2. 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. +3. Di line 5, terdapat *stubbing* untuk fungsi `service.disburseMoney(DisbursementRequest)` untuk mengembalikan objek `paymentLinkResponse`. +4. Di line 6, dilakukan *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? + +1. Apakah fungsi `paymentCallback` memanggil **setidaknya satu kali** *helper function* `disburseMoney` untuk akses API Flip? (sintaks verifikasi pemanggilan *mock* ada di line 10) +2. Apakah fungsi `paymentCallback` memanggil **setidaknya satu kali** fungsi `save` pada `PaymentRepository` untuk menyimpan perubahan objek `Payment` ke dalam database? (sintaks verifikasi pemanggilan *mock* ada di line 11) 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"` +### Contoh 3: Fungsi `paymentCallback` sukses menyimpan status `WAITING_DISBURSEMENT` ketika status Payment Link `"SUCCESSFUL"` dan status Disbursement `"DONE"` ```java @Test void testPaymentCallbackWithSuccessfulStatusAndSuccessfulDisbursement() { @@ -307,52 +337,63 @@ Selain itu, kita juga perlu *set status* pada objek `disbursementResponse` menja } ``` -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*. +Berikut adalah penjelasan terkait *test case* ini: + +1. Di line 3, terdapat *stubbing* untuk fungsi `paymentRepository.findByPaymentLinkId(Integer)` untuk menyimulasikan bahwa di database terdapat objek `payment`. +2. 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. +3. Di line 5, terdapat *stubbing* untuk fungsi `service.disburseMoney(DisbursementRequest)` untuk mengembalikan objek `paymentLinkResponse`. +4. Di line 6, dilakukan *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? + +1. Apakah fungsi `paymentCallback` memanggil **setidaknya satu kali** *helper function* `disburseMoney` untuk akses API Flip? (sintaks verifikasi pemanggilan *mock* ada di line 10) +2. Apakah fungsi `paymentCallback` memanggil **setidaknya satu kali** fungsi `save` pada `PaymentRepository` untuk menyimpan perubahan objek `Payment` ke dalam database? (sintaks verifikasi pemanggilan *mock* ada di line 11) 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* `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. +- [ ] 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. +- [ ] 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. + +### Latihan Mandiri: Buat sebuah *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? + - [ ] Apakah fungsi `payToOtherDestination` memanggil *helper function* `getPaymentLink` untuk akses API Flip? + - [ ] Apakah fungsi `payToOtherDestination` memanggil fungsi `save` pada `PaymentRepository` untuk menyimpan objek `Payment` ke dalam database? + - [ ] Apakah fungsi `payToOtherDestination` mengembalikan objek `Payment`, dengan isi yang sama seperti yang dikembalikan oleh API Flip? -### Latihan Mandiri: Buat *negative case* +### Latihan Mandiri: Buat beberapa *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"`. + - [ ] Apakah fungsi `paymentCallback` memanggil **setidaknya satu kali** *helper function* `getPaymentLink` untuk akses API Flip? + - [ ] Apakah fungsi `paymentCallback` memanggil **setidaknya satu kali** fungsi `save` pada `PaymentRepository` untuk menyimpan perubahan objek `Payment` ke dalam database? + - [ ] 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 /> + - [ ] 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"`. + - [ ] Apakah fungsi `paymentCallback` memanggil **setidaknya satu kali** fungsi `save` pada `PaymentRepository` untuk menyimpan perubahan objek `Payment` ke dalam database? + - [ ] 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*). + +Untuk mengecek apakah aplikasi mengeluarkan *exception*, kita bisa memanfaatkan fungsi `assertThrows`. 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. +### Contoh 1: `payToUser` pada `PaymentServiceImpl` akan mengembalikan *error* ketika user ID dari *sender* dan *destination* sama. ```java @Test void testPayToUserFailWhenSenderAndDestinationAreTheSame() { @@ -364,7 +405,7 @@ Sebagai contoh, berikut adalah beberapa *negative case* yang bisa Anda gunakan: *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. +### Contoh 2: `payToUser` pada `PaymentServiceImpl` akan mengembalikan *error* jika Flip API sedang tidak bisa diakses. ```java @Test void testPayToUserThrowsExceptionWhenAPIIsUnreachable() { @@ -378,13 +419,17 @@ Sebagai contoh, berikut adalah beberapa *negative case* yang bisa Anda gunakan: } ``` -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`. +Berikut adalah penjelasan mengenai *stubbing* pada *test case* ini: + +1. Di line 3, terdapat *stubbing* untuk fungsi `userRepository.findById(Integer)` untuk mengembalikan objek *mock* `user`. +2. Di line 4, 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. +1. Apakah fungsi `payToUser` meneruskan *exception* `APIUnreachableException`? (pengecekan di line 6) +2. Apakah fungsi `payToUser` memanggil *helper function* `getPaymentLink` 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. ```java @Test void testPaymentCallbackWithSuccessfulStatusThrowsExceptionWhenAPIIsUnreachable() { @@ -399,25 +444,35 @@ Selain itu, juga terdapat *stubbing* untuk fungsi `service.getPaymentLink(Paymen } ``` -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`. +Berikut adalah penjelasan mengenai *stubbing* pada *test case* ini: + +1. Di line 3, terdapat *stubbing* untuk fungsi `paymentRepository.findByPaymentLinkId(Integer)` untuk mengembalikan objek `payment`. +2. Di line 4, dilakukan *set status* pada objek `paymentCallbackRequest` menjadi `"SUCCESSFUL"`, + karena akses API Flip untuk *disbursement* hanya akan diakses jika status *payment link* sudah `SUCCESSFUL`. +3. Di line 5, Selain itu, juga terdapat *stubbing* untuk fungsi `service.disburseMoney(DisbursementRequest)` untuk melempar *exception* `APIUnreachableException`. + Ini untuk menyimulasikan ketika API Flip sedang tidak bisa diakses. + *Test case* ini akan mengecek: -1. Apakah fungsi `payToUser` meneruskan *exception* `APIUnreachableException`? -2. Apakah fungsi `payToUser` memanggil *helper function* `disburseMoney` untuk akses API Flip? + +1. Apakah fungsi `payToUser` meneruskan *exception* `APIUnreachableException`? (pengecekan di line 8) +2. Apakah fungsi `payToUser` memanggil *helper function* `disburseMoney` untuk akses API Flip? (sintaks verifikasi pemanggilan *mock* ada di line 11) ### 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`. +- [ ] 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 tiga hal yang disebutkan di arahan. +- [ ] 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. +- [ ] 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. ### 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? + - [ ] Apakah fungsi `payToOtherDestination` meneruskan *exception* `APIUnreachableException`? + - [ ] Apakah fungsi `payToOtherDestination` memanggil **setidaknya satu kali** *helper function* `getPaymentLink` untuk akses API Flip? ## *Refactoring*: Bagaimana jika SIBAYAR bisa menggunakan lebih dari satu Payment Gateway? @@ -427,76 +482,98 @@ 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`, 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. +- [ ] Buat *interface* baru `PaymentGatewayAPI` di package `com.example.sibayar.external.paymentgateway`, dengan menggunakan *snippet* berikut: + ```java + public interface PaymentGatewayAPI { + PaymentLinkResponse getPaymentLink(PaymentLinkRequest request); + DisbursementResponse disburseMoney(DisbursementRequest request); + } + ``` + Tujuan dari *interface* `PaymentGatewayAPI` adalah untuk memastikan bahwa semua *class* penghubung dengan API *payment gateway* dapat diakses dengan cara yang sama. +- [ ] Buat *class* baru `FlipAPI` di packakge `com.example.sibayar.external.paymentgateway`, 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()); + } + } + ``` + Lakukan juga beberapa hal berikut: + + - [ ] Pindahkan juga isi *method* `getPaymentLink` yang sebelumnya ada di `com.example.sibayar.service.payment.PaymentServiceImpl`, ke dalam *method* `getPaymentLink` yang ada di `FlipAPI`. + - [ ] Pindahkan juga isi *method* `disburseMoney` yang sebelumnya ada di `com.example.sibayar.service.payment.CallbackServiceImpl`, ke dalam *method* `getPaymentLink` yang ada di `FlipAPI`. + +- [ ] Pada `PaymentServiceImpl` dan `CallbackServiceImpl`, buat koneksi baru ke implementasi dari `PaymentGatewayAPI` dengan cara membuat *instance variable* baru. Gunakan *snippet* berikut untuk diletakkan di definisi *class* dari `PaymentServiceImpl` dan `CallbackServiceImpl`. + ```java + @Qualifier("flipAPI") + private final PaymentGatewayAPI api; + ``` + `@Qualifier("flipAPI")` akan otomatis melakukan *dependency injection* sehingga variabel `api` akan berisi objek dari `FlipAPI`. +- [ ] Gunakan `api` untuk mengakses fungsi `getPaymentLink` dan `disburseMoney` yang telah dipindahkan ke `FlipAPI`. Misal dari sebelumnya: + ```java + PaymentLinkResponse apiResponse = getPaymentLink(apiRequest); + ``` + menjadi seprti berikut: + ```java + PaymentLinkResponse apiResponse = api.getPaymentLink(apiRequest); + ``` - [ ] 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*. + ```java + @Mock + private PaymentGatewayAPI flipAPI; + ``` +- [ ] Gunakan objek *mock* `flipAPI` untuk menggantikan pemanggilan fungsi `getPaymentLink` dan `disburseMoney` pada setiap *test case*. Misal dari sebelumnya: + ```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*. Cek apakah dependensi eksternal seperti *repository* atau *payment gateway* API dipanggil sesuai kebutuhan setiap *test case*. +- [ ] Integrasikan *project* Anda dengan SonarQube.<br /> + 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? -- [ ] 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*? + +- [ ] 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). [1]: https://docs.google.com/presentation/d/1f1vpyYOu3GSyIeyqGLU-D6REBn0ACN81?rtpof=true&usp=drive_fs +[2]: https://docs.flip.id/ diff --git a/docs/workshops/images/day_2_tdd_-_sonarqube_sibayar.png b/docs/workshops/images/day_2_tdd_-_sonarqube_sibayar.png new file mode 100644 index 0000000000000000000000000000000000000000..25a4475c93314ce884dfcdcc51c87492b9ffd3f9 Binary files /dev/null and b/docs/workshops/images/day_2_tdd_-_sonarqube_sibayar.png differ