Skip to content

Day 1: Software Quality Assurance (SQA) Overview

Test-Driven Development (TDD) dan Penjaminan Mutu

Dalam workshop tiga hari ini, anda akan diperkenalkan dengan metode Test-Driven Development (TDD) dan Behavior-Driven Development (BDD) yang dapat diterapkan sebagai bagian dari proses pengembangan software. Masing-masing mengedepankan membuat skenario test terlebih dahulu sebelum mulai mengimplementasikan sebuah spesifikasi fitur.

Perbedaannya TDD dan BDD terletak pada pelaku dan proses penyusunan skenario uji coba. TDD biasanya dibuat oleh tim pengembang dan mengujicobakan komponen software secara white-box ataupun black-box. Sedangkan BDD dibuat melalui kolaborasi antara tim pengembang dengan pemangku kepentingan (stakeholder) lain yang bersifat non-teknis, serta diimplementasikan menggunakan kombinasi program dan prosa bahasa natural.

TDD akan dibahas secara singkat di hari pertama workshop, kemudian dibahas lebih mendalam di hari kedua workshop. Sedangkan BDD akan dibahas di hari ketiga workshop. Fokus hari ini adalah paparan TDD secara umum dan kegiatan penjaminan mutu.

Persiapan Workshop

Persiapkan tools berikut di komputer anda:

Pastikan anda dapat memanggil program-program berikut dari dalam shell favorit anda:

1
2
3
4
git --version
java --version
javac --version
mvn --version

Hasil pemanggilan program-program di atas seharusnya akan mencetak versi program yang terpasang di komputer anda.

Selain itu, pastikan anda telah memiliki akun di GitLab CSUI dan SonarQube CSUI. Pastikan anda bisa berhasil login ke masing-masing sistem.

Mari mulai dengan menyalin contoh proyek yang akan dibahas di hari pertama workshop, yaitu Sitodo PMPL. Buka laman proyek Sitodo PMPL di tautan berikut (klik), lalu klik tombol "Fork" untuk membuat salinan proyek tersebut ke dalam akun/namespace GitLab CSUI anda.

Apabila sudah membuat fork proyek Sitodo PMPL, buka laman fork proyek tersebut di GitLab CSUI dan salin repositori kode proyeknya ke suatu lokasi di sistem berkas komputer anda menggunakan Git:

1
2
3
# Contoh perintah Git untuk membuat salinan repositori ke dalam sebuah folder
# baru bernama `sitodo-pmpl` di dalam direktori home:
git clone https://gitlab.cs.ui.ac.id/[ akun GitLab CSUI anda ]/sitodo-pmpl.git ~/sitodo-pmpl

Jika anda lebih nyaman menggunakan IDE seperti IntelliJ IDEA, anda juga dapat menyalin repositori kode proyek melalui tombol "Get from VCS" seperti yang digambarkan pada screenshot berikut:

Tampilan tombol "Get from VCS"

Selanjutnya, buka kode proyek menggunakan IntelliJ IDEA. Kode proyek yang akan dibahas di hari pertama workshop adalah aplikasi "Sitodo PMPL", yaitu aplikasi Todo List sederhana yang dibangun menggunakan framework Spring Boot dan digunakan sebagai running example di dalam mata kuliah Penjaminan Mutu Perangkat Lunak di Fasilkom UI.

Panduan untuk membuat build serta menjalankan aplikasi dapat dibaca secara mandiri di dokumentasi proyek (README.md). Namun untuk keperluan workshop hari ini, anda tidak perlu memasang database PostgreSQL yang dibutuhkan oleh aplikasi. Sebagai gantinya, anda akan menggunakan database in-memory bernama HSQLDB yang akan selalu di-reset setiap kali aplikasi dimatikan.

Untuk membuat build dan menjalankan aplikasinya secara lokal menggunakan database HSQLDB, panggil perintah Maven untuk membuat build terlebih dahulu:

1
mvn -DskipTests package

Kemudian jalankan berkas JAR aplikasinya:

1
java -jar ./target/sitodo-0.1.4-SNAPSHOT.jar

Catatan: Jika ingin mencoba menjalankan aplikasinya menggunakan database PostgreSQL, silakan tambah opsi -D"spring.profiles.active=postgresql di pemanggilan perintah java -jar.

Aplikasi akan jalan dan dapat diakses melalui alamat http://127.0.0.1:8080. Apabila sudah ada aplikasi lain yang jalan di alamat yang sama (misal: bentrok nomor port), tambahkan parameter -D"server.port=<nomor port lain> ketika memanggil perintah Maven.

Contoh tampilan awal aplikasi dapat dilihat pada screenshot berikut:

Contoh tampilan aplikasi Sitodo PMPL

Selanjutnya, coba menjalankan fitur utama aplikasi, yaitu membuat todo list. Tambahkan beberapa item baru ke dalam todo list. Kemudian perhatikan kondisi-kondisi pada aplikasi, seperti:

  • Alamat (URL) yang tercantum di address bar pada web browser yang anda gunakan.
  • Pesan yang muncul setelah anda mengubah status penyelesaian item di dalam todo list.
  • URL aplikasi ketika anda melakukan refresh atau mengunjungi kembali aplikasi di alamat http://127.0.0.1:8080.

Test Automation

Langkah-langkah percobaan yang anda lakukan sebelumnya mungkin berbeda dengan apa yang dilakukan oleh kolega anda. Mungkin anda membuat item baru dengan mengetikkan item tersebut kemudian anda menekan tombol "Enter" di keyboard. Sedangkan kolega anda tidak menekan tombol "Enter" ketika membuat item baru, melainkan menekan tombol "Enter" yang ada di halaman aplikasi. Mungkin skenario di atas terdengar sepele, namun menggambarkan adanya potensi proses uji coba dilakukan secara tidak konsisten jika dilakukan oleh manusia.

Langkah-langkah yang cenderung repetitif dapat diotomasi dan dijalankan oleh bantuan program test. Program tidak akan "lelah" ketika harus menjalankan instruksi yang sama berulang kali. Bayangkan fitur membuat todo list baru tersebut diuji coba secara otomatis setiap kali ada perubahan baru pada kode proyek. Tim pengembang dapat lebih fokus untuk menyelesaikan fitur-fitur yang dibutuhkan dan menyiapkan prosedur uji coba yang dibutuhkan untuk dijalankan secara otomatis.

Saat ini kode proyek Sitodo PMPL telah memiliki kumpulan test suite, yaitu kumpulan test case yang dapat dijalankan sebagai program test oleh test runner terhadap subjek uji coba. Subjek uji coba berupa software/sistem secara utuh (seringkali disebut sebagai System/Software Under Test atau SUT).

Struktur Test Case

Sebuah test case yang diimplementasikan sebagai program test biasanya akan memiliki struktur yang terdiri dari empat bagian prosedur, yaitu:

  1. Setup - menyiapkan testing environment dan SUT ke kondisi siap diuji coba, termasuk menyiapkan nilai masukan test case
  2. Exercise - menjalankan skenario uji coba pada SUT
  3. Verify - membuktikan hasil skenario uji coba pada SUT dengan hasil yang diharapkan
  4. Teardown - mengembalikan kondisi testing environment dan SUT ke kondisi awal sebelum uji coba

Mari coba identifikasi keempat bagian tersebut pada dua contoh test case. Pertama, lihat test case berikut yang membuktikan kebenaran method equals pada class TodoItem:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Test
void testEquals() {
    // Setup
    TodoItem first = new TodoItem("Buy milk");
    TodoItem second = new TodoItem("Cut grass");

    // Exercise (implisit) & Verify
    assertNotEquals(first, second);

    // Tidak ada Teardown secara eksplisit
}

Setup mengandung instruksi untuk menyiapkan SUT, yaitu membuat dua buah objek TodoItem yang berperan sebagai subjek yang akan diujicobakan. Kemudian Exercise dilakukan secara implisit ketika Verify dilakukan pada contoh di atas, yaitu pemanggilan assertNotEquals akan memanggil implementasi equals milik masing-masing SUT dan membandingkan hasil kembaliannya. Pada contoh di atas, tidak ada prosedur Teardown secara eksplisit. Namun, anda bisa menganggap proses garbage collection yang dilakukan runtime Java (JVM) di akhir eksekusi test sebagai prosedur Teardown.

Mari lihat contoh test case lain yang lebih kompleks, yaitu test case untuk class MainController:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@WebMvcTest(MainController.class)   // <-- Setup
class MainControllerTest {

    @Autowired  // <-- Setup
    private MockMvc mockMvc;

    @Test
    @DisplayName("HTTP GET '/' redirects to '/list")
    void showMainPage_resolvesToIndex() throws Exception {
        mockMvc.perform(get("/"))   // <-- Exercise
               .andExpectAll(   // <-- Verify
                   status().is3xxRedirection(),
                   redirectedUrl("/list")
               );
        // Tidak ada Teardown secara eksplisit
    }
}

Prosedur Setup pada test case di atas melakukan:

  1. @WebMvcTest menyiapkan environment minimalis berupa server untuk menjalankan SUT (yaitu: objek MainController) beserta dependency yang dibutuhkan oleh SUT.
  2. @Autowired menyiapkan objek mock bertipe MockMvc sebagai client untuk menyimulasikan pertukaran pesan HTTP Request & Response terhadap SUT.

Sedangkan prosedur Exercise cukup jelas, yaitu menggunakan mockMvc untuk mengirimkan HTTP GET Request ke URL /. HTTP GET Request tersebut akan diterima oleh SUT, yaitu objek MainController. Kemudian prosedur Verify mengandung beberapa kondisi akhir yang akan dibuktikan dengan menginspeksi HTTP Response yang dikembalikan oleh SUT.

Setelah mengetahui struktur test case secara umum, mari membahas TDD secara garis besar dengan melihat contoh beberapa test, yaitu unit test dan functional test.

Unit Test

Mari coba lihat contoh test suite yang termasuk dalam golongan unit test. Unit dalam konteks ini mengacu pada komponen terkecil pada software. Sebagai contoh, fungsi dan metode (method) dapat diklasifikasikan sebagai unit.

Jalankan perintah Maven berikut di shell untuk menjalankan test suite bertipe unit:

1
mvn test -D"groups=unit"

Catatan: Bagi pengguna IntelliJ IDEA, anda dapat membuat Run Configuration untuk menjalankan perintah Maven di atas.

Maven akan menjalankan test suite yang berisi kumpulan test case dari grup @Tag("unit") di kode test. Hasil eksekusi setiap test case kemudian dilaporkan ke standard output dan berkas-berkas laporan di folder target/surefire-reports.

Isolasi Pada Unit Test

Idealnya sebuah test case pada unit test suite akan menguji unit secara terisolasi. Isolasi test disini mengacu pada praktik untuk hanya menguji sebuah unit tanpa mengikutsertakan unit lain yang dibutuhkan. Namun pada praktiknya, batasan isolasi bisa "dilanggar" untuk membuat prosedur uji coba terhadap sebuah unit lebih tepat guna.

Ambil contoh misalnya ketika menguji implementasi fungsi yang bertanggung jawab sebagai request handler pada komponen controller di Spring Boot. Apabila mengikuti idealisme membuat unit test yang murni terisolasi, maka uji coba pada request handler akan berisi prosedur yang membandingkan string nama berkas template HTML yang akan dikirimkan sebagai HTTP Response.

Apakah salah jika unit test pada fungsi request handler hanya sekedar membandingkan string nama berkas template HTML? Tentu saja tidak. Unit test tersebut bisa bermanfaat untuk menjamin developer agar menggunakan nama berkas template HTML yang tepat. Secara idealisme, unit test tersebut juga memenuhi kriteria isolasi murni sehingga test bisa berjalan dengan cepat tanpa mengikutsertakan unit lain ataupun dependency yang dibutuhkan untuk menjalankan fungsi request handler.

Jika ingin benar-benar menguji perilaku request handler, yakni memastikan fungsinya bisa menerima masukan berupa HTTP Request dan mengembalikan HTTP Response dengan tepat, maka batasan isolasi bisa dilanggar dengan membuat prosedur uji coba agar berjalan di dalam server aplikasi yang menjalankan framework Spring Boot secara minimal. Oleh karena itu, unit test suite bagi komponen-komponen controller di proyek ini diberikan anotasi @WebMvcTest. Anotasi tersebut bertujuan untuk menjalankan framework Spring Boot dan server aplikasi ketika test case berjalan sehingga komponen controller dapat menerima HTTP Request dan mengembalikan HTTP Response.

Functional Test

Sekarang coba jalankan test suite untuk menguji fungsionalitas pada SUT, atau seringkali dikenal sebagai functional test. Pengujian dilakukan terhadap SUT yang sudah di-build dan berjalan di sebuah environment.

Jalankan perintah Maven berikut di shell:

1
mvn test -D"groups=func"

Serupa dengan contoh eksekusi sebelumnya, Maven akan menjalankan test suite yang berisi kumpulan test case dari grup @Tag("func") di kode test. Test suite jenis ini disebut sebagai functional test, dimana test case akan menggunakan web browser untuk menjalankan aksi-aksi pengguna terhadap SUT. Pada contoh aplikasi Sitodo PMPL, aksi-aksi pengguna dijalankan pada web browser secara otomatis dengan bantuan library Selenium. Oleh karena itu, anda akan melihat web browser anda bergerak secara otomatis ketika functional test berjalan.

Jika anda ingin menjalankan seluruh test suite, maka perintah Maven yang dapat anda panggil adalah sebagai berikut:

1
mvn test

Unit test akan berjalan sangat cepat dimana durasi tiap eksekusi test case berada dalam rentang kurang dari 1 detik per test case. Sedangkan functional test akan memakan waktu lebih lama karena ada overhead untuk menyiapkan dan menyimulasikan aksi pengguna di web browser.

Laporan Hasil Test

Test suite pada proyek Sitodo PMPL dibuat dengan bantuan test framework JUnit 5. Sebagai test framework, JUnit 5 memberikan kerangka kepada developer agar dapat membuat test suite sesuai dengan konvensi JUnit 5. Selain itu, JUnit 5 juga menyediakan test runner untuk menjalankan test suite serta dapat melakukan test reporting untuk mencatat hasil eksekusi test suite.

Laporan hasil test dapat dilihat di folder target/surefire-reports. Anda dapat temukan berkas-berkas laporan dalam format teks polos (.txt) dan XML. Berkas laporan teks polos hanya menyebutkan berapa banyak test case yang berhasil dan gagal pada sebuah test suite. Sedangkan berkas laporan XML mengandung informasi yang jauh lebih rinci, seperti informasi environment yang menjalankan test case hingga cuplikan log standard output ketika menjalankan test case.

Berkas-berkas laporan tersebut dapat dikumpulkan dan diberikan ke tools lain. Sebagai contoh, pipeline CI/CD GitLab dapat membaca berkas laporan XML JUnit. Berkas-berkas laporan XML tersebut akan dipetakan menjadi daftar test case yang diasosiasikan ke pipeline CI/CD GitLab. Contoh tampilan daftar tersebut dapat dilihat pada screenshot berikut ini:

Contoh daftar test case di pipeline

Siklus TDD

Proses pengembangan yang menerapkan metode TDD akan melalui tiga fase berikut secara iteratif ketika mengimplementasikan sebuah fitur, yaitu:

  1. Fase "Red"
  2. Fase "Green"
  3. Fase "Refactor"

Fase "Red" adalah fase awal dimana developer mengembangkan test case untuk implementasi sebuah fitur terlebih dahulu. "Red" mengacu pada status gagal/fail yang dilaporkan oleh test runner secara visual. Tentu saja hasil test akan gagal karena kode implementasi masih kosong.

Setelah mengembangkan test case yang dibutuhkan, developer masuk ke fase "Green" dimana developer mengembangkan kode implementasi fitur sehingga test case berhasil/pass.

Fase terakhir, yaitu fase "Refactor", bertujuan untuk meningkatkan kualitas kode serta menjaga agar test case-test case yang sudah ada tidak kembali gagal/fail.

Untuk memberikan gambaran lebih konkrit mengenai fase-fase TDD, mari coba berlatih mengimplementasikan sebuah fitur sederhana dengan gaya TDD, yaitu fitur "Visitor Counter" yang akan menghitung dan menampilkan jumlah pengunjung yang pernah membuka aplikasi.

Buat branch baru untuk keperluan pengerjaan latihan selama workshop. Misalnya, buat branch baru bernama workshop dengan perintah Git berikut:

1
2
3
git checkout -b workshop
# Alternatif perintah Git jika menggunakan program Git versi terkini:
git switch -c workshop
Kemudian ikuti instruksi pada latihan masing-masing fase berikut secara berurutan.

Latihan Singkat: Fase "Red"

Misalnya deskripsi fitur "View Counter" dalam gaya user story adalah sebagai berikut: "As a user, I would like to see how frequent the app has been visited." Kita dapat membayangkan bahwa pengguna perlu dapat melihat jumlah kunjungan pada aplikasi. Untuk keperluan workshop ini, mari coba implementasikan secara sederhana saja, yaitu dengan memunculkan sebuah pesan berisi angka kunjungan di halaman Todo List dan jumlah data kunjungan tidak perlu disimpan ke dalam database.

Pertama, buka class Java TodoListControllerTest untuk mulai menambahkan test case baru, yaitu test case yang membuktikan bahwa jumlah kunjungan akan dilampirkan ke dalam template HTML halaman Todo List. Test case dituliskan sebagai method baru bernama showList_countFirstVisit sebagai berikut:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Test
@DisplayName("First visit to /list should produce a correct string in the HTML page")
void showList_countFirstVisit() throws Exception {
    mockMvc.perform(get("/list"))
           .andExpectAll(
               status().isOk(),
               content().contentTypeCompatibleWith(TEXT_HTML),
               content().encoding(UTF_8),
               content().string(containsString("This list has been viewed 1 time"))
           );
}

Kemudian coba jalankan test case baru tersebut dari dalam IDE dengan menekan tombol di samping baris deklarasi test case tersebut, seperti yang digambarkan pada screenshot berikut:

Contoh menjalankan sebuah test case di IDE

Hasil dari test case tersebut pasti akan gagal/fail. Jangan khawatir karena fase "Red" pada TDD memang mengharapkan awal dari implementasi setiap fitur harus gagal terlebih dahulu. Developer kemudian akan mengimplementasikan fitur dengan benar di fase berikutnya ("Green") sehingga lulus/pass setiap test case yang ada.

Silakan simpan terlebih dahulu hasil pekerjaan dengan membuat commit Git. Pesan commit bisa disesuaikan agar menunjukkan bahwa saat ini anda baru saja menyelesaikan fase "Red" TDD, misal: "[RED] Write test for displaying visit counter"

Latihan Singkat: Fase "Green"

Sebelum mulai membuat implementasi dengan benar, mari pikirkan sebuah solusi paling sederhana yang akan meluluskan test case di atas. Jika mengacu pada kode test saat ini, test case di atas akan melakukan verifikasi pada halaman HTML dengan mencari kemunculan teks "This list has been viewed 1 time". Supaya test case dapat lulus, maka salah satu solusi paling sederhana (dan juga paling naif) adalah menambahkan teks yang diharapkan di dalam dokumen template HTML yang akan dikirimkan sebagai HTTP Response.

Buka berkas HTML bernama list.html dan tambahkan kode HTML berikut di antara penutup tag </form> dan penutup tag </div>:

1
2
3
<footer>
    <p class="text-muted" id="view_count">This list has been viewed 1 time.</p>
</footer>

Kemudian jalankan kembali test case dan lihat hasilnya. Hasilnya pasti akan lulus/pass.

Secara TDD, solusi naif di atas memang sah tapi hanya berlaku ketika halaman Todo List hanya dikunjungi satu kali. Oleh karena itu, mari coba menambahkan test case baru yang menyimulasikan ketika halaman Todo List sudah pernah dibuka dua kali. Buka kembali class Java TodoListControllerTest dan tambahkan method baru berikut sebagai test case kunjungan dilakukan dua kali:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Test
@DisplayName("Second visit to /list should produce a correct string in the HTML page")
void showList_countSecondVisit() throws Exception {
    mockMvc.perform(get("/list"))
           .andExpectAll(
               status().isOk(),
               content().contentTypeCompatibleWith(TEXT_HTML),
               content().encoding(UTF_8),
               content().string(containsString("This list has been viewed 1 time"))
           );

    mockMvc.perform(get("/list"))
           .andExpectAll(
               status().isOk(),
               content().contentTypeCompatibleWith(TEXT_HTML),
               content().encoding(UTF_8),
               content().string(containsString("This list has been viewed 2 time"))
           );
}

Jalankan test case baru tersebut. Hasilnya pasti gagal, karena halaman HTML saat ini hanya mengandung "This list has been viewed 1 time".

Untuk membuat test case baru tersebut lulus, maka solusi naif yang bisa diikuti adalah menambahkan teks yang diharapkan di dalam dokumen template HTML. Tapi pikirkan kembali, apakah mau membuat implementasi fitur hingga tuntas dengan cara tersebut? Oleh karena itu, mari ubah kembali dokumen HTML sehingga pesan jumlah kunjungannya dihasilkan secara dinamis berdasarkan perhitungan yang dilakukan oleh backend aplikasi. Silakan ubah kembali dokumen HTML list.html dengan mengganti isi tag <p> dengan sintaks Thymeleaf sehingga teks yang ditampilkan mengandung nominal angka kunjungan yang dihitung dari backend. Contoh kode HTML versi akhirnya dapat dilihat pada potongan kode berikut:

1
2
3
4
5
6
<footer>
    <p class="text-muted" id="view_count" th:if="${viewCount}"
       th:text="'This list has been viewed ' + ${viewCount} + ' time(s)'">
        This list has been viewed 0 time(s).
    </p>
</footer>

Jalankan kembali test case. Hasilnya akan kembali gagal karena implementasi saat ini mengharapkan nilai viewCount dari backend aplikasi.

Sekarang buatlah sebuah class Java baru bernama ViewCounterService di dalam package com.example.sitodo.service. Objek dari class Java ini akan berperan sebagai service yang menyediakan implementasi business logic perhitungan kunjungan. Isi dari class Java tersebut dapat mengikuti kode berikut:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.example.sitodo.service;

import org.springframework.stereotype.Service;

import java.util.concurrent.atomic.AtomicInteger;

@Service
public class ViewCounterService {

    private final AtomicInteger viewCount = new AtomicInteger(0);

    public int incrementAndGet() {
        return viewCount.incrementAndGet();
    }
}

Setelah membuat service class baru, buka class Java TodoListController dan tambahkan field yang akan menampung objek service baru tersebut. Sebagai contoh, potongan kode berikut menambahkan field baru bernama viewCounterService dan setter method terkait dengan konfigurasi dependency injection melalui setter method:

1
2
3
4
5
6
private ViewCounterService viewCounterService;

@Autowired
public void setViewCounterService(ViewCounterService viewCounterService) {
    this.viewCounterService = viewCounterService;
}

Kemudian perbaharui implementasi method showList yang menerima URL tanpa parameter dan dengan parameter. Tambahkan statement untuk menambahkan atribut viewCount yang mengandung perhitungan jumlah kunjungan. Contoh kodenya adalah sebagai berikut:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@GetMapping("/list")
public String showList(TodoList todoList, Model model) {
    model.addAttribute("todoList", todoList);
    model.addAttribute("motivationMessage", todoListService.computeMotivationMessage(todoList));
    model.addAttribute("viewCount", viewCounterService.incrementAndGet());

    return "list";
}

@GetMapping("/list/{id}")
public String showList(@PathVariable("id") Long id, Model model) {
    TodoList foundTodoList = todoListService.getTodoListById(id);

    model.addAttribute("todoList", foundTodoList);
    model.addAttribute("motivationMessage", todoListService.computeMotivationMessage(foundTodoList));
    model.addAttribute("viewCount", viewCounterService.incrementAndGet());

    return "list";
}

Sekarang jalankan test case yang telah dibuat sebelumnya. Hasilnya akan kembali gagal karena TodoListController membutuhkan objek ViewCounterService ketika test berjalan. Oleh karena itu, buka kembali berkas TodoListControllerTest dan tambahkan mock object ViewCounterService sebagai field:

1
2
@MockBean
private ViewCounterService viewCounterService;

Kemudian perbaharui instruksi pada test case agar menyimulasikan perilaku ViewCounterService seakan-akan melakukan perhitungan kunjungan. Tambahkan potongan kode berikut sebelum memanggil objek mockMvc di dalam method showList_countFirstVisit:

1
when(viewCounterService.incrementAndGet()).thenReturn(1);

Lalu tambahkan kode serupa di dalam method showList_countSecondVisit:

1
2
3
4
5
// Sebelum pemanggilan mockMvc pertama
when(viewCounterService.incrementAndGet()).thenReturn(1);

// Sebelum pemanggilan mockMvc kedua
when(viewCounterService.incrementAndGet()).thenReturn(2);

Versi akhir method showList_countFirstVisit dan showList_countSecondVisit adalah sebagai berikut:

 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
@Test
@DisplayName("First visit to /list should produce a correct string in the HTML page")
void showList_countFirstVisit() throws Exception {
    when(viewCounterService.incrementAndGet()).thenReturn(1);
    mockMvc.perform(get("/list"))
           .andExpectAll(
               status().isOk(),
               content().contentTypeCompatibleWith(TEXT_HTML),
               content().encoding(UTF_8),
               content().string(containsString("This list has been viewed 1 time"))
           );
}

@Test
@DisplayName("Second visit to /list should produce a correct string in the HTML page")
void showList_countSecondVisit() throws Exception {
    when(viewCounterService.incrementAndGet()).thenReturn(1);
    mockMvc.perform(get("/list"))
           .andExpectAll(
               status().isOk(),
               content().contentTypeCompatibleWith(TEXT_HTML),
               content().encoding(UTF_8),
               content().string(containsString("This list has been viewed 1 time"))
           );

    when(viewCounterService.incrementAndGet()).thenReturn(2);
    mockMvc.perform(get("/list"))
           .andExpectAll(
               status().isOk(),
               content().contentTypeCompatibleWith(TEXT_HTML),
               content().encoding(UTF_8),
               content().string(containsString("This list has been viewed 2 time"))
           );
}

Jalankan kembali test case yang menyimulasikan satu kali dan dua kali kunjungan. Hasilnya akan lulus/pass. Simpan hasil pekerjaan dengan membuat commit Git dengan pesan commit seperti [GREEN] Implement visit counter, lalu push ke fork.

Fase "Refactor"

Setelah menuntaskan fase "Red" dan "Green", developer dapat memasuki fase "Refactor" untuk memperbaiki desain kode yang telah dibuat. Fase "Refactor" mengacu pada kegiatan refactoring yang bertujuan untuk meningkatkan kualitas kode tanpa merusak kebenaran kode tersebut. Fase ini dibutuhkan karena proses implementasi yang dilakukan pada fase "Green" seringkali fokus untuk meluluskan test dengan cepat sehingga kode yang dituliskan mungkin dibuat tanpa mempertimbangkan aspek kualitas.

Potensi perbaikan yang dapat dilakukan bisa diidentifikasi dari ada tidaknya code smells pada kode. Proses identifikasi dapat dilakukan oleh developer berdasarkan pengalaman dan pengetahuan, ataupun dibantu dengan tools yang dapat menganalisis kualitas kode.

Contoh-contoh code smells yang umum ditemukan antara lain:

  • Duplikasi pada kode, seperti ada kumpulan statement identik yang dituliskan berulang kali di beberapa method dalam sebuah class Java.
  • Penamaan yang kurang deskriptif, seperti memberikan nama variabel dengan nama yang tidak bermakna.
  • Penerapan praktik yang sudah kuno, seperti menggunakan @RequestMapping pada controller Spring Boot.

Latihan Mandiri: Membuat Test Pada Objek Service

Ingat kembali bahwa anda telah membuat implementasi controller dan service di satu siklus TDD yang dijelaskan di atas. Saat ini test suite hanya mencakup pembuktian kebenaran implementasi controller, sedangkan service belum memiliki test suite terkait.

Tugas anda selanjutnya adalah membuat test case untuk service perhitungan kunjungan, yaitu class Java ViewCounterService. Langkah-langkah pengerjaannya adalah sebagai berikut:

  1. Buat class Java baru bernama ViewCounterServiceTest di dalam folder src/test/java/com/example/sitodo/service.
  2. Buat satu test case yang akan menguji kebenaran implementasi perhitungan kunjungan yang dilakukan oleh objek service. Misalnya dengan membuat method baru bernama testIncrementAndGet di dalam class ViewCounterServiceTest.
  3. Tambahkan anotasi @Test di atas method baru tersebut.
  4. Tuliskan statement Java yang akan membuat objek service yang akan diujicobakan di dalam method baru.
  5. Implementasikan prosedur uji coba yang memanggil method perhitungan kunjungan lalu membuktikan bahwa hasil akhirnya sesuai dengan ekspektasi. Misalnya setelah memanggil method incrementAndGet, maka buktikan kembalian dari pemanggilan method tersebut sesuai dengan ekspektasi anda.
  6. Jalankan test case tersebut dan pastikan verifikasi berhasil dilakukan.

Contoh solusi dari latihan di atas dapat dilihat di contoh kode berikut:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.example.sitodo.service;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class ViewCounterServiceTest {

    @Test
    void testIncrementAndGet_once() {
        ViewCounterService viewCounterService = new ViewCounterService();

        int result = viewCounterService.incrementAndGet();

        assertEquals(1, result);
    }
}

Catatan: Mungkin anda akan bertanya mengapa contoh solusi tidak memiliki anotasi @WebMvcTest. Alasannya adalah pengujian pada objek service kebetulan tidak membutuhkan server ataupun dependency terhadap komponen yang disediakan framework Spring Boot. Oleh karena itu, objek service dapat diujicobakan secara independen sebagai objek Java, atau dikenal sebagai Plain Old Java Object (POJO).

Jangan lupa untuk menyimpan hasil pekerjaan. Buat commit Git, lalu push ke fork.

Analisis & Laporan Kualitas Kode

Setelah melihat bagaimana menerapkan metode TDD pada proses pengembangan software sekarang mari mengenali bagaimana kualitas kode dapat dianalisis dan dijamin menggunakan bantuan tools seperti GitLab CI/CD dan SonarQube.

Saat ini contoh proyek Sitodo PMPL telah memiliki konfigurasi pipeline CI/CD dengan beberapa stage sebagai berikut:

  1. build - menjamin kode proyek dapat di-build menjadi berkas JAR aplikasi.
  2. test - menjalankan test suite bertipe unit test sekaligus melakukan analisis terhadap kode proyek
  3. deploy - menjalankan deployment ke server tujuan
  4. post-deploy - menjalan test suite BDD, akan dibahas pada workshop hari ketiga
  5. report - melaporkan hasil test dan analisis kode ke GitLab dan SonarQube

Sebagai bagian dari penjaminan kualitas, pipeline CI/CD dapat dirancang agar menjalankan serangkaian program secara otomatis setiap kali ada perubahan pada kode proyek. Salah satu kasus yang dapat diantisipasi dengan adanya pipeline CI/CD adalah melakukan mitigasi terhadap regresi pada aplikasi. Eksekusi test secara otomatis dapat membuktikan bahwa perubahan terbaru pada kode proyek tidak merusak kondisi kode saat ini. Namun tentu saja mitigasi terhadap regresi baru bisa tercapai jika kode proyek memang mengandung test dengan cakupan yang menyeluruh dan kode test memang dituliskan dengan tujuan untuk menjamin kebenaran implementasi.

Salah satu tolok ukur "cakupan pengujian" yang umum digunakan di industri adalah line coverage, yaitu persentase baris atau statement kode proyek yang telah dijalankan oleh test suite. Jika melihat pada nilai line coverage kode proyek Sitodo PMPL, saat ini line coverage bernilai 97%. Artinya adalah 97% statement kode pada proyek sudah pernah dijalankan oleh test suite.

Kualitas kode proyek juga dapat dianalisis dari bagaimana kode proyek dituliskan. Analisis kualitas kode dapat dilakukan melalui tool yang didukung oleh SonarQube, yaitu SonarScanner. Cara kerja SonarScanner adalah sebagai berikut:

  1. SonarScanner melakukan analisis terhadap kode program dan mengumpulkan laporan-laporan dari test framework serta hasil analisis dari tools lain yang didukung oleh SonarQube.
  2. Hasil analisis SonarScanner dan laporan-laporan lainnya diagregatkan sebelum dikirim ke SonarQube.
  3. SonarQube menerima laporan agregat dari SonarScanner dan mengolahnya untuk menentukan kualitas kode.
  4. Hasil pengolahan SonarQube akan ditampilkan di laman proyek analisis terkait.

Berikut ini adalah contoh tampilan dashboard yang menampilkan gambaran besar kualitas kode proyek Sitodo PMPL di SonarQube:

Contoh dashboard SonarQube

Dapat dilihat bahwa proyek Sitodo PMPL memenuhi kriteria kualitas kode bawaan SonarQube yang dikenal sebagai Quality Gate. Menurut dokumentasi SonarQube, sebuah proyek dianggap lulus Quality Gate jika:

  • Tidak ada bug baru pada kode.
  • Tidak ada celah keamanan baru pada kode.
  • Semua isu terkait keamanan ("security hotspot") sudah ditindaklanjuti.
  • Perubahan baru pada kode mengandung technical debt/code smells di bawah batasan tertentu.
  • Perubahan baru pada kode mengandung duplikasi kode di bawah batasan tertentu.
  • Perubahan baru pada kode di-cover oleh test.

Prinsip yang ditekankan oleh kriteria Quality Gate adalah "Clean as You Code". Apabila sudah sempat memenuhi Quality Gate, maka selanjutnya tim pengembang diharapkan untuk, minimal, mempertahankan kualitas kode yang telah tercapai di setiap perubahan yang dilakukan. Jika sudah berhasil mempertahankan kualitas, maka tim pengembang juga diharapkan untuk memperbaiki isu-isu yang sudah sempat terdeteksi.

Latihan Singkat: Membuat Proyek Analisis Baru di SonarQube

Saat ini kode proyek Sitodo PMPL masih diatur untuk mengirimkan hasil analisis SonarScanner ke proyek analisis SonarQube milik organisasi perkuliahan PMPL. Mari mencoba untuk membuat proyek analisis baru yang akan menampung analisis SonarScanner dari fork anda:

  1. Masuk ke SonarQube CSUI
  2. Pilih "Add Project" > "Manually" dari laman depan daftar proyek di SonarQube.
  3. Masukkan "Project Key" dan "Display Name" dengan format sitodo-pmpl-[nama anda]. Kemudian pilih "Set up". Misalnya, screenshot berikut menggambarkan pembuatan "Project Key" dengan nama sitodo-pmpl-bambang: Contoh membuat project key
  4. Di layar berikutnya, pilih "Generate a token" dan salin nilai token yang dibuatkan oleh SonarQube.
  5. Setelah membuat token, pilih opsi analisis menggunakan Maven. SonarQube akan mencantumkan contoh perintah Maven yang dapat dipanggil untuk melakukan analisis secara lokal, seperti yang digambarkan pada screenshot berikut: Contoh perintah Maven untuk menjalankan SonarScanner Silakan dicoba apabila ingin memastikan analisis SonarScanner berhasil dikirimkan ke SonarQube dari komputer anda.

Selanjutnya, anda perlu memperbaharui konfigurasi Maven dan pipeline CI/CD agar bisa membuat analisis SonarScanner dilakukan dengan benar di lingkungan CI:

  1. Buka pom.xml kode proyek Sitodo PMPL, lalu ubah nilai property sonar.projectKey dengan "Project Key" anda.
  2. Buka .gitlab-ci.yml kode proyek Sitodo PMPL, lalu ubah rules: pada job CI sonarqube-check agar job tersebut akan berjalan ketika ada commit baru yang di-push ke branch workshop. Versi final konfigurasi sonarqube-check dapat dilihat di potongan kode berikut:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    sonarqube-check:
       stage: report
       image: docker.io/library/maven:3.9.5-eclipse-temurin-17-focal
       variables:
         SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
         GIT_DEPTH: "0"
       rules:
         # Jalankan SonarScanner pada commit-commit baru yang di-push ke branch workshop ataupun branch utama
         # Namun sebaiknya, hanya salah satu branch saja. Jangan dua-duanya.
         - if: $CI_COMMIT_BRANCH == 'workshop'
         - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
       script:
         - mvn -P sonar sonar:sonar
       cache:
         key: "${CI_JOB_NAME}"
         paths:
           - .sonar/cache
       dependencies:
         - build
         - test
    
  3. Buka konfigurasi variabel CI/CD di laman proyek fork Sitodo PMPL dan tambahkan dua variabel baru bernama SONAR_TOKEN dan SONAR_HOST_URL. Laman konfigurasi variabel CI/CD dapat diakses di URL: https://gitlab.cs.ui.ac.id/[akun GitLab CSUI anda]/sitodo-pmpl/-/settings/ci_cd Isi SONAR_TOKEN dengan token yang dibuat sebelumnya, lalu isi SONAR_HOST dengan alamat SonarQube CSUI (https://sonarqube.cs.ui.ac.id).

Simpan pekerjaan anda. Buat commit Git dan push ke fork.

Latihan Singkat: Mengurangi Tingkat Kompleksitas Kode Melalui Refactoring

Dengan waktu yang terbatas, mari fokus mengulas sebuah contoh code smell, yaitu Cyclomatic/Cognitive Complexity. Perhatikan contoh kode berikut:

 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
public String computeMotivationMessage(TodoList todoList) {
    List<TodoItem> items = todoList.getItems();
    final long totalItems = (items != null) ? items.size() : 0;
    final long totalFinishedItems = (items != null) ?
        items.stream().filter(TodoItem::getFinished).count() : 0;

    LOG.debug("Total Items: {}; Total Finished Items: {}", totalItems, totalFinishedItems);

    String output = "";

    if (totalItems == 0) {
        output += emptyListMessage;
    } else if (totalItems < manyItemsThreshold) {
        output += fewItemsMessage;

        if (totalFinishedItems == totalItems) {
            output += " " + allFinishedMessage;
        } else if (totalFinishedItems == 0) {
            output += " " + noFinishedMessage;
        } else if (totalFinishedItems < totalItems && totalFinishedItems >= totalItems / 2) {
            output += " " + halfFinishedMessage;
        } else {
            output += someFinishedMessage;
        }
    } else {
        output += manyItemsMessage;

        if (totalFinishedItems == totalItems) {
            output += " " + allFinishedMessage;
        } else if (totalFinishedItems == 0) {
            output += " " + noFinishedMessage;
        } else if (totalFinishedItems < totalItems && totalFinishedItems >= totalItems / 2) {
            output += " " + halfFinishedMessage;
        } else {
            output += someFinishedMessage;
        }
    }

    LOG.debug("Resulting output: {}", output);

    return output;
}

Implementasi fungsi computeMotivationMessage di atas memiliki nilai Cognitive Complexity sebesar 17 menurut temuan SonarScanner. Angkat tersebut diperoleh dengan menerapkan heuristik yang telah disusun oleh SonarScanner. Ide dasar perhitungan nilai kompleksitas pada tolok ukur Cognitive Complexity adalah semakin banyak tingkat kedalaman sebuah percabangan, maka akan semakin kompleks kode tersebut. Oleh karena itu, tingkat kompleksitas pada kode dapat dikurangi dengan cara menyederhanakan percabangan.

Dapat dilihat bahwa terdapat percabangan yang mengandung duplikasi, yaitu pembuatan pesan motivasi berdasarkan jumlah tugas yang tersisa. Bagian tersebut dapat dienkapsulasi ke dalam sebuah fungsi baru. Dalam literatur refactoring, prosedur melakukan enkapsulasi satu atau lebih statement ke dalam sebuah fungsi disebut sebagai Extract Method.

Buka kode Java terkait yang mengandung sumber computeMotivationMessage menggunakan IntelliJ IDEA kemudian highlight seluruh blok percabangan yang diawali oleh if (totalFinishedItems == totalItems) {, seperti yang terlihat pada screenshot berikut dari IntelliJ:

Blok percabangan yang akan di-refactor

Apabila blok tersebut sudah di-highlight, klik kanan dan pilih opsi Refactoring > Extract Method. Kemudian berikan nama fungsi yang mengandung hasil enkapsulasi, lalu pilih opsi untuk mengganti ("Replace") semua blok percabangan yang duplikat dengan pemanggilan fungsi baru.

Catatan: Extract Method juga dapat dilakukan melalui keyboard shortcut Ctrl+Alt+M di IntelliJ.

Versi akhir fungsi computeMotivationMessage setelah refactoring adalah sebagai berikut:

 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
public String computeMotivationMessage(TodoList todoList) {
    List<TodoItem> items = todoList.getItems();
    final long totalItems = (items != null) ? items.size() : 0;
    final long totalFinishedItems = (items != null) ?
        items.stream().filter(TodoItem::getFinished).count() : 0;

    LOG.debug("Total Items: {}; Total Finished Items: {}", totalItems, totalFinishedItems);

    String output = "";

    if (totalItems == 0) {
        output += emptyListMessage;
    } else if (totalItems < manyItemsThreshold) {
        output += fewItemsMessage;
        output = getMotivationMessageByTotalItems(totalFinishedItems, totalItems, output);
    } else {
        output += manyItemsMessage;
        output = getMotivationMessageByTotalItems(totalFinishedItems, totalItems, output);
    }

    LOG.debug("Resulting output: {}", output);

    return output;
}

private String getMotivationMessageByTotalItems(long totalFinishedItems, long totalItems,
                                                String output) {
    if (totalFinishedItems == totalItems) {
        output += " " + allFinishedMessage;
    } else if (totalFinishedItems == 0) {
        output += " " + noFinishedMessage;
    } else if (totalFinishedItems < totalItems && totalFinishedItems >= totalItems / 2) {
        output += " " + halfFinishedMessage;
    } else {
        output += someFinishedMessage;
    }

    return output;
}

Untuk memastikan kegiatan refactoring tidak merusak kode implementasi sehingga menyebabkan regresi, jalankan kembali test suite yang menguji kebenaran implementasi fungsi computeMotivationMessage. Jika hasil eksekusi test suite sukses/pass, maka dapat disimpulkan refactoring berhasil dilakukan tanpa merusak kebenaran implementasi. Sebaliknya, jika ternyata hasilnya gagal, maka ada kesalahan pada refactoring yang dilakukan.

Latihan Mandiri: Membersihkan Beberapa Code Smells

Jika masih ada waktu tersisa, maka lihat koleksi code smells yang telah diidentifikasi dan dilaporkan ke proyek SonarQube anda. Kemudian, pilih minimal tiga buah code smells dan coba perbaiki code smells tersebut. Untuk setiap perbaikan sebuah code smell, simpan pekerjaan anda sebagai commit Git dengan pesan commit yang mencantumkan nama code smell terkait.

Berikut ini ada beberapa kategori code smells yang dapat diperbaiki, terurut berdasarkan tingkat kesulitan, dan dapat diselesaikan dalam waktu singkat:

  • Remove this public modifier - menghapus visibility modifier public pada class Java yang tidak akan digunakan dari luar modul
  • Declare this local variable with var instead - mengganti deklarasi variabel bertipe data dengan var, yaitu sintaks Java baru yang muncul sejak Java versi 10.
  • Define a constant instead of duplicating this literal string n times - membuat sebuah variabel konstan berisi string yang akan digunakan berulang kali
  • This for loop can be replaced by a foreach loop - mengganti blok perulangan for menjadi enhanced-for di Java

Penutup

Anda sudah mencoba secara garis besar penerapan TDD dan aktivitas penjaminan kualitas dengan bantuan tools. Sebelum mengakhiri workshop, jangan lupa menyimpan hasil pekerjaan sebagai commit Git dan push ke fork.

Untuk bahan diskusi saat refleksi:

  • Apa saja kendala Bapak/Ibu dalam pekerjaan saat ini yang mungkin dapat diperbaiki dengan menerapkan materi yang dipelajari hari ini?
  • Jika sudah pernah menerapkan test automation, apa kendala Bapak/Ibu saat ini dalam membuat kode test dan menjalankan test suite?
  • Setelah melihat contoh-contoh code smells yang diidentifikasi oleh SonarQube, apakah semua code smells perlu ditindaklanjuti?

Hari kedua workshop akan fokus mendalami TDD, terutama pada implementasi kode test terisolasi dengan teknik mocking.


Last update: 2023-11-02 09:29:41
Created: 2023-10-17 02:48:29