Test Driven Development dari Sudut Pandang Mahasiswa Ilmu Komputer

LyzanderAndrylie
19 min readMar 5, 2024

--

Photo by Scott Graham on Unsplash

Dalam perjalanan Anda, setidaknya saya, sebagai mahasiswa jurusan Ilmu Komputer ataupun Teknik Informatika, kita akan menjumpai istilah yang cukup umum digunakan dalam pengembangan perangkat lunak, yaitu test driven development (TDD). Secara sederhana, TDD adalah pengembangan perangkat lunak yang mengedepankan pembuatan test terlebih dahulu sebelum kode produksi. Namun, apakah TDD ini penting? Apa saja keuntungan dari TDD? Bagaimana Implementasi TDD? Bagaimana trend TDD saat ini? Hal inilah yang akan saya bahas pada blog ini.

Test Driven Development a.k.a TDD

Seperti yang telah dijelaskan sebelumnya, test driven development (TDD) adalah pengembangan perangkat lunak yang mengedepankan pembuatan test terlebih dahulu sebelum kode produksi. Walaupun terdengar cukup mudah, kenyataannya banyak mahasiswa Ilmu Komputer (semoga bukan saya) yang kerap kali mengabaikan TDD dalam berbagai proyek pengembangan perangkat lunak. Nah, apa saja sih kesulitan yang dihadapi oleh mahasiswa Ilmu Komputer dalam menerapkan displin TDD ini? Yuk, kita simak!

TDD sendiri memiliki tiga aturan dalam pengimplementasiannya sebagai berikut.

  1. Aturan Pertama: Anda tidak boleh menulis kode produksi sampai Anda telah menulis unit test yang gagal.
  2. Aturan Kedua: Anda tidak boleh menulis lebih banyak unit test dari yang dibutuhkan untuk gagal, dan kode tidak dapat dikompilasi juga termasuk gagal.
  3. Aturan Ketiga: Anda tidak boleh menulis kode produksi lebih dari yang diperlukan untuk gagal dalam unit test yang telah dibuat.

Catatan: unit test adalah bentuk pengujian yang menguji unit individu dari kode yang kita buat. Unit individu di sini dapat berupa suatu function, method pada class, dan component antarmuka pada frontend framework seperti React.

Keuntungan Disiplin TDD

Berdasarkan aturan yang telah dijabarkan sebelumnya, tentu kita dapat mengetahui beberapa keuntungan dari disiplin TDD. Beberapa keuntungan yang ada adalah sebagai berikut.

  1. Kode Anda memiliki pengujian dan pernah diuji
    Ya! Ini salah satu keuntungan yang akan dirasakan oleh saya, sebagai mahasiswa Ilmu Komputer, ketika menerapkan TDD pada proyek yang telah saya buat. Ketika kode yang kita buat sudah sangat banyak, kita akan kesulitan jika harus menguji setiap bagian kode secara manual. Nah, dengan memiliki unit test, kita tinggal menjalankan ulang semua unit test yang telah dibuat. Menjalankan semua unit test ulang ini biasanya dikenal sebagai regression testing.
  2. Pengecekan efek samping terhadap perubahan kode
    Kita seringkali melakukan perubahan kode pada proyek yang kita buat. Ada banyak alasan mengapa kita melakukan perubahan kode, seperti perbaikan kode yang sudah ada tanpa mengubah logika (refactoring) dan perbaikan karena ada kesalahan logika (logic error). Namun, setelah perbaikan dilakukan, bagaimana kita dapat mengetahui bahwa kode kita telah berjalan sesuai dengan yang diharapkan dan tidak ada efek samping? Jika kita menerapkan TDD, kita tinggal menjalankan ulang semua unit test yang telah dibuat. Jika tidak? Ya, berdoa aja semoga tidak ada bug yang menunggu atau cek semua fitur secara manual (tidak ada cara lain).
  3. Pelan-pelan dan berpikir
    Salah satu hal yang sering dialami oleh saya sebagai mahasiswa Ilmu Komputer adalah pengin cepat selesai mengerjakan tugas-tugas yang ada, termasuk tugas pengembangan perangkat lunak. Nah, masalahnya adalah ketika kita ingin cepat selesai, kadang kala, kita mengabaikan hal-hal yang cukup penting demi cepat selesai. Akibatnya, hasil perangkat lunak yang kita buat menjadi tidak maksimal dan memiliki peluang adanya bug.
    Jika kita menerapkan TDD, kita akan “dipaksa” berpikir terlebih dahulu, setidaknya berkaitan dengan input yang akan diterima kode, bagaimana kode memproses input, dan bagaimana kode menghasilkan output. Walaupun hasil akhir tetap ditentukan dari pengalaman dan kemampuan, setidaknya kode yang dihasilkan akan jauh lebih baik dibandingkan dengan tidak menerapkan TDD dan unit test sama sekali.

Apakah masih ada keuntungan lainnya? Tentu saja! Namun, sebagai mahasiswa Ilmu Komputer, saya paling merasakan tiga keuntungan yang telah disebutkan di atas.

Kesulitan dari TDD

Berdasarkan penjabaran sebelumnya, kita telah memahami terkait dengan keuntungan dari TDD. Namun, tetap saja terdapat kesulitan yang dihadapi oleh mahasiswa Ilmu Komputer dalam menerapkan displin TDD ini. Kesulitan-kesulitan tersebut adalah sebagai berikut.

  1. Waktu untuk membuat unit test
    Idealnya, satu siklus unit test (penerapan aturan 1, 2, 3 dan kembali ke 1) diharapkan dapat diselesaikan dalam jangka waktu 30 detik berdasarkan Martin, R. C. (2009) selaku pembuat buku Clean Code. Namun, kenyataannya hal tersebut tidak secepat yang diharapkan. Ada berbagai faktor yang menghambat pembuatan unit test hingga menyebakan pembuatan unit test memakan waktu lebih lama dibandingkan dengan implementasi. Hal ini akan dijelaskan pada bagian berikutnya.
  2. Tidak mengetahui bagaimana implementasi suatu fungsionalitas tertentu
    Sebagai programmer, kita tidak bisa mengetahui implementasi detail dari setiap fitur atau fungsionalitas yang diharapkan. Misalnya, bagaimana kita menghubungkan aplikasi yang kita miliki dengan penyedia layanan autentikasi, seperti Google? Banyak implementasi yang dapat dilakukan dan masing-masing implementasi bervariasi. Lalu, bagaimana kita bisa membuat unit test terlebih dahulu tanpa memahami bagaimana implementasi autentikasi tersebut dilakukan? Pada akhirnya, kita diharapkan untuk mengetahui gambaran besar terlebih dahulu sebelum melakukan implementasi. Hal ini tentu bagus. Namun, bagaimana bila kita dikejar dengan deadline? Apakah kita harus mementingkan unit test atau fitur terlebih dahulu?
  3. Mocking objek dari berbagai library yang memakan waktu
    Mocking atau yang dikenal dengan istilah test double seringkali digunakan untuk menggantikan implementasi asli dari sebuah kode tertentu. Hal ini dikarenakan kita ingin test berjalan dengan cepat dan dapat terprediksi, dan dapat direplikasi. Bayangkan jika setiap eksekusi test akan melakukan pemanggilan jaringan. Tentu hal tersebut sangat buruk (lambat + tidak terprediksi + tidak dapat direplikasi). Masalahnya adalah ada beberapa library yang sulit di-mock. Salah satu contoh nyata akan dibahas di bagian bawah terkait dengan implementasi TDD dalam proyek PPL.
  4. Kendala teknis yang berkaitan dengan teknologi
    Walaupun hal ini jarang terjadi, tidak menutup kemungkinan suatu kode tidak dapat atau sulit dilakukan unit test. Salah satu yang saya alami sekarang adalah bagaimana menguji async react server component yang dapat dibilang sebagai teknologi baru pada Next.js. Bahkan, pada dokumentasi dituliskan sebagai berikut.

Since async Server Components are new to the React ecosystem, some tools do not fully support them. In the meantime, we recommend using End-to-End Testing over Unit Testing for async components.
- Next.js on Async Server Components

Seberapa Pentingkah TDD?

Menurut saya, sebagai mahasiswa Ilmu Komputer, TDD cukup penting untuk memastikan kebenaran kode yang dibuat. Kita telah melihat berbagai keuntungan yang diperoleh dari TDD. Jika TDD dilakukan dengan baik, kita sebagai programmer akan memperoleh keuntungan yang besar dalam pengembangan perangkat lunak.

Namun, TDD bukanlah satu-satunya cara dalam melakukan testing. Misalnya, berkaitan dengan autentikasi yang telah disebutkan, salah satu library autentikasi pada Next.js, yaitu NextAuth.js merekomendasikan pengujian dalam bentuk integration testing (pengujian dilakukan dengan menguji integrasi antara unit-unit individu dari kode yang kita buat). Selain itu, kita harus mengingat bahwa 100% coverage dari unit test tidak menjami kode kita dapat bekerja sebagaimana mestinya. Pada akhirnya, pengujian akan dilakukan dengan berbagai pengujian lainnya, seperti integration testing dan functional testing.

100% code coverage does not guarantee 100% case coverage.

Developers targeting 100% code coverage are chasing the wrong metric.

Eric Elliott on Mocking is a Code Smell

Taken from https://victor.kropp.name/blog/100-percent-code-coverage/

Jadi, apakah kita harus selalu melakukan TDD di setiap proyek? Jawabannya cukup sederhana, yaitu tergantung dengan kebutuhan dan apakah memungkinkan. Kadang kala, kondisi yang bersifat praktikal lebih diinginkan dibandingkan dengan kondisi yang bersifat ideal. Jika kita memiliki deadline terkait dengan proyek, tentu lebih baik kita mengutamakan fitur selesai. Namun, bukan berarti TDD tidak penting, tetapi lebih kepada kita harus menyesuaikan dan fleksibel dengan kondisi yang terjadi saat ini.

Taken from https://makeameme.org/meme/test-coverage-100

Implementasi TDD

Dengan semua hal yang berkaitan dengan TDD telah diuraikan, mari kita lanjut bagaimana implementasi TDD dilakukan. Namun, sebelum kita mencoba melakukan implementasi, kita harus mengetahui alur bagaimana TDD dilakukan.

Red-Green-Refactor TDD cycle by TDD Manifesto

Perkenalkan TDD Mantra — Red/Green/Refactor.

  1. Red (berkaitan dengan aturan 1): tulis unit test kecil yang tidak berfungsi (gagal), dan bahkan mungkin tidak dikompilasi pada awalnya.
  2. Green (berkaitan dengan aturan 2 dan 3): buatlah tes bekerja/berhasil dengan cepat, melakukan apa pun yang diperlukan dalam prosesnya.
  3. Refactor: menghilangkan semua duplikasi dan code smell yang dibuat hanya untuk membuat tes bekerja pada tahap sebelumnya. Tahapan ini bersifat opsional.

Dengan pemahaman terkait TDD Mantra, kita sudah siap untuk mencoba implementasi TDD. Berkaitan dengan contoh implementasi TDD, kita akan mencoba menerapkan TDD untuk mengimplementasi fungsi pembagian angka yang merupakan fungsi yang sederhana (tidak mungkin juga kita mengimplementasikan algoritma kompleks hanya untuk mendemonstrasikan TDD).

Dalam demonstrasi ini, sebagai mahasiswa Ilmu Komputer yang tertarik dengan web development, saya akan menggunakan JavaScript sebagai bahasa pemrograman dan Jest sebagai testing framework. Selain itu, tahapan demonstrasi akan dilakukan dalam dua siklus saja.

Red #1

Pada tahap ini, kita akan mengimplementasikan aturan pertama dari TDD, yaitu Anda tidak boleh menulis kode produksi sampai Anda telah menulis unit test yang gagal. Namun, bagaimana cara kita menulis unit test yang baik? Secara sederhana, unit test dibuat untuk menguji logika dari program dan pengujian harus bersifat terisolasi yang berarti kita hanya menguji satu unit individu saja, baik function, method, dan class. Karena kita ingin mengimplementasi fungsi pembagian angka yang merupakan fungsi yang sederhana, unit individu yang dimaksud adalah fungsi pembagian berikut.

// divide.js
/**
* Simple division function
* @param {number} dividend
* @param {number} divisor
*/
export function divide(dividend, divisor) {

}

Kode di atas dibuat agar fungsi divide() dapat di-import ke file test, yaitu divide.test.js. Berikut adalah test yang dibuat untuk menguji operasi pembagian benar atau tidak.

// divide.test.js
import { divide } from "./divide";

describe('Divide', () => {
it('should return 5 when dividing 10 by 2', () => {
expect(divide(10, 2)).toBe(5)
});
});

Jika kita menjalankan kode test di atas, test di atas tentu saja akan gagal karena kita belum melakukan implementasi apa pun pada fungsi divide(). Namun, hal itu juga berarti kita telah memenuhi aturan pertama dari TDD, yaitu telah menulis unit test yang gagal. Selain itu, kita juga memenuhi aturan kedua, yaitu kita tidak menulis lebih banyak unit test dari yang dibutuhkan untuk gagal. Dengan demikian, kita dapat lanjut ke tahap berikutnya, yaitu green.

// Output dari Jest
FAIL src/divide.test.js
Divide
× should return 5 when dividing 10 by 2 (3 ms)

● Divide › should return 5 when dividing 10 by 2

expect(received).toBe(expected) // Object.is equality

Expected: 5
Received: undefined

3 | describe('Divide', () => {
4 | it('should return 5 when dividing 10 by 2', () => {
> 5 | expect(divide(10, 2)).toBe(5)
| ^
6 | });
7 | });

Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 0.69 s, estimated 9 s

Green #1

Sesuai dengan aturan ketiga, kita tidak boleh menulis kode produksi lebih dari yang diperlukan untuk gagal dalam unit test yang telah dibuat. Lalu, bagaimana agar fungsi divide() kita berhasil (pass) dengan kode produksi seminimal mungkin? Ya, gunakan saja operator pembagian yang tersedia di JavaScript.

/**
* Simple division function
* @param {number} dividend
* @param {number} divisor
*/
export function divide(dividend, divisor) {
return dividend / divisor;
}

Setelah implementasi dibuat, kita tinggal menjalankan kembali unit test yang telah dibuat untuk menguji implementasi tersebut.

// Output dari Jest
> tdd-demo@1.0.0 test
> jest divide.test.js

PASS src/divide.test.js
Divide
√ should return 5 when dividing 10 by 2 (4 ms)

Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.768 s, estimated 1 s

Karena pengujian berhasil, kita akan lanjut ke tahap berikutnya, yaitu refactor.

Refactor #1

Apakah refactor diperlukan untuk implementasi ini? Tidak. Seperti yang kita ketahui, tahap refactor bersifat opsional. Jika kode yang diimplementasikan telah baik dan sesuai dengan standar, kita bisa melewati tahap ini.

Red #2

Kita telah sampai sejauh ini untuk implemetasi fungsi divide(). Namun, apakah fungsi divide() telah bekerja sebagaimana mestinya? Bagaimana bila kita memanggil fungsi divide() dengan dividend berupa 0? Nah, untuk mengatasi hal tersebut, kita akan masuk ke siklus TDD berikutnya, yaitu Red lagi.


// divide.test.js
import { divide } from "./divide";

describe('Divide', () => {
it('should return 5 when dividing 10 by 2', () => {
expect(divide(10, 2)).toBe(5)
});

// Unit test tambahan
it('should return null when dividing any number by 0', () => {
expect(divide(10, 0)).toBe(null)
});
});

Jika kita menjalankan test-nya kembali, kita akan mendapatkan output sebagai berikut.

> tdd-demo@1.0.0 test
> jest divide.test.js

FAIL src/divide.test.js
Divide
√ should return 5 when dividing 10 by 2 (2 ms)
× should return null when dividing any number by 0 (4 ms)

● Divide › should return null when dividing any number by 0

expect(received).toBe(expected) // Object.is equality

Expected: null
Received: 5

7 |
8 | it('should return null when dividing any number by 0', () => {
> 9 | expect(divide(10, 2)).toBe(null)
| ^
10 | });
11 | });

Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: 0.821 s, estimated 1 s

Green #2

Karena unit test yang ditulis telah gagal, kita bisa lanjut ke tahap ini dan membuat implementasi agar kode test di atas dapat berhasil (pass). Berikut adalah kode implementasi divide.().

// divide.js
/**
* Simple division function
* @param {number} dividend
* @param {number} divisor
*/
export function divide(dividend, divisor) {
if (divisor !== 0) {
return dividend / divisor;
} else {
return null;
}
}

Jika kita menjalankan test-nya kembali, kita akan mendapatkan output sebagai berikut.

> tdd-demo@1.0.0 test
> jest divide.test.js

PASS src/divide.test.js
Divide
√ should return 5 when dividing 10 by 2 (3 ms)
√ should return null when dividing any number by 0

Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.745 s, estimated 1 s

Refactor #2

Apakah kita memerlukan tahap ini? Ya dan tidak. Jika Anda sudah oke dengan kode implementasi, Anda dapat melewatkan tahap ini. Namun, jika kita melihat pada kode implementasi fungsi divide(), kita dapat memperbaiki kode tersebut sehingga lebih mudah untuk dibaca, yaitu dengan menerapkan konsep return early.

Return early is the way of writing functions or methods so that the expected positive result is returned at the end of the function and the rest of the code terminates the execution (by returning or throwing an exception) when conditions are not met.
Leonel Menaia Dev on Return Early Pattern

Berikut adalah kode fungsi divide() setelah dilakukan proses refactoring.

// divide.js
/**
* Simple division function
* @param {number} dividend
* @param {number} divisor
*/
export function divide(dividend, divisor) {
if (divisor === 0) return null;

return dividend / divisor;
}

Jika kita menjalankan test-nya kembali, kita akan mendapatkan output sebagai berikut yang menandakan proses refactoring tidak menghasilkan efek samping yang tidak diharapkan.

> tdd-demo@1.0.0 test
> jest divide.test.js

PASS src/divide.test.js
Divide
√ should return 5 when dividing 10 by 2 (2 ms)
√ should return null when dividing any number by 0

Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.622 s, estimated 1 s

Yes! Proses TDD dalam dua siklus ini selesai dan sudah cukup untuk mencakup berbagai kasus dalam pembagian, yaitu kasus pembagian dengan dua bilangan non-negatif sebagai positive test case dan kasus pembagian dengan pembagi bernilai 0 sebagai negative test case.

Implementasi TDD pada Dunia Nyata

Setelah memahami dan mencoba TDD, kalian mungkin mengira bahwa proses TDD dapat berjalan dengan lancar, seperti halnya dalam disiplin TDD untuk fungsi divide() sebelumnya. Namun, hal ini tidak selalu benar. Kadang kala, kita akan menemukan berbagai masalah dalam implementasi TDD yang melibatkan berbagai library dan framework yang kompleks.

Salah satu contoh yang saya alami dalam tugas proyek pengembangan perangkat lunak adalah bagaimana menguji component antarmuka pada React yang memanfaatkan fungsi (hook) berupa useFormState(). Perhatikan bahwa React Server Component dan fungsi useFormState() merupakan teknologi baru pada saat blog ini dibuat sehingga pencarian bantuan di internet, seperti StackOverflow, cukup sedikit dan sulit untuk ditemukan.

"use client";

import { useFormState} from "react-dom";
import { authenticate } from "@/lib/action";

export function LoginForm() {
const [errorMessage, dispatch] = useFormState(authenticate, undefined);

return (
<form action={dispatch}>
<h2>Login Form</h2>
<p>
Otherwise, just fill the form below!
</p>
<LoginFormControl errorMessage={errorMessage} />
</form>
);
}

Secara sederhana, berkaitan dengan component di atas, kita ingin menguji tampilan antarmuka yang di-render oleh React apakah sesuai dengan yang kita harapkan. Pada kasus di atas, saya ingin menguji apakah component antarmuka yang merepresentasikan LoginForm memiliki input untuk email, input untuk password, dan button untuk melakukan login. Lalu, bagaimana cara mengujinya?

Untuk menguji kasus di atas, kita harus menggunakan beberapa library, yaitu jest dan jest-environment-jsdom agar dapat menguji component React tersebut. Library jest-environment-jsdom mengandung fungsi-fungsi yang diperlukan untuk menguji component React. Selanjutnya, karena useFormState() tidak diuji dalam test ini, kita memanfaatkan mocking untuk fungsi tersebut. Hasil implementasi unit test-nya adalah sebagai berikut.

import { LoginForm } from "@/components/auth/LoginForm";
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/react";

jest.mock("react-dom", () => {
const originalModule = jest.requireActual("react-dom");

return {
...originalModule,
useFormState: (action: unknown, initialState: unknown) => [
undefined,
mockedDispact,
],
useFormStatus: jest.fn().mockReturnValue({
pending: true,
data: null,
method: null,
action: null,
}),
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
Events: [],
},
};
});

describe("LoginForm", () => {
it("renders without crashing", () => {
render(LoginForm());

const headingElement = screen.getByRole("heading", { name: /Login Form/i });
expect(headingElement).toBeInTheDocument();
expect(headingElement).toHaveTextContent("Login Form");

const email = screen.getByLabelText("Email");
expect(email).toBeInTheDocument();

const emailPlaceholder = screen.getByPlaceholderText("Email");
expect(emailPlaceholder).toBeInTheDocument();

const password = screen.getByLabelText("Password");
expect(password).toBeInTheDocument();

const passwordPlaceHolder = screen.getByPlaceholderText("Password");
expect(passwordPlaceHolder).toBeInTheDocument();

const loginButton = screen.getByTestId("login");
expect(loginButton).toBeInTheDocument();
});
});

Kalau Anda perhatikan, ada salah satu objek yang harus di-mock agar proses pengujian dapat berjalan dengan baik, yaitu __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED. Mungkin muncul pertanyaan, apa itu? Sejujurnya saya juga tidak tau, wkwk. Namun, objek tersebut harus di-mock dan satu-satunya cara mengetahui objek tersebut harus di-mock adalah dengan mencari langsung ke kode sumber atau mencari berbagai issue mengenai mocking useFormState()yang telah diajukan di Github. Hal inilah kadang yang menghambat proses pengembangan perangkat lunak yang mana TDD seharusnya membantu programmer dalam mengembangkan kode malah menyebabkan kesulitan bagi programmer. Namun, apabila kesulitan sebanding dengan keuntungan yang diperoleh, kenapa tidak?

Taken from https://bytedev.medium.com/things-ive-learned-from-writing-a-lot-of-unit-tests-2d234d0cfccf

Apakah pengujian cukup dengan melakukan TDD saja? Nyatanya, dalam dunia nyata, kita harus memperhatikan aspek lain, seperti code coverage dan CI/CD. Apa itu dan bagaimana hal itu dilakukan? Mari, kita simak!

Code Coverage

Code coverage adalah metrik yang digunakan untuk mengukur seberapa banyak kode kita telah diuji. Seperti yang telah disebutkan sebelumnya, code coverage pada kode kita tidak harus 100% dan code coverage > 90% saja sudah cukup. Bahkan, menurut Eric Elliott dalam artikel Mocking is a Code Smell, semakin code coverage mendekati 100%, maka semakin kecil manfaat yang diperoleh dan semakin besar kode test yang kita buat menjadi kompleks dan bersifat tightly coupled. Apa itu tightly coupled? Secara sederhana, ketika perubahan yang kalian lakukan pada kode kalian menyebabkan perubahan pada berbagai test yang telah kalian buat, maka kode test yang kalian buat termasuk tightly coupled.

Setelah mengetahui code coverage, lalu, bagaimana cara mengecek code coverage? Kalian dapat menggunakan testing framework yang digunakan dalam pengembangan aplikasi kalian. Contohnya adalah sebagai berikut.

> frontend@0.1.0 test:coverage
> jest --coverage
---------------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------------------|---------|----------|---------|---------|-------------------
All files | 100 | 94.59 | 95.83 | 100 |
app | 100 | 100 | 100 | 100 |
page.tsx | 100 | 100 | 100 | 100 |
app/(protected) | 100 | 100 | 100 | 100 |
layout.tsx | 100 | 100 | 100 | 100 |
app/(protected)/home | 100 | 100 | 100 | 100 |
page.tsx | 100 | 100 | 100 | 100 |
app/login | 100 | 100 | 66.66 | 100 |
error.tsx | 100 | 100 | 50 | 100 |
page.tsx | 100 | 100 | 100 | 100 |
app/register | 100 | 100 | 100 | 100 |
error.tsx | 100 | 100 | 100 | 100 |
page.tsx | 100 | 100 | 100 | 100 |
---------------------------|---------|----------|---------|---------|-------------------

Test Suites: 17 passed, 17 total
Tests: 101 passed, 101 total
Snapshots: 0 total
Time: 9.683 s
Ran all test suites.

Namun, apakah itu saja sudah cukup? Tentu tidak. Apakah kalian ingin menjalankan test dan code coverage secara manual? Jika Anda programmer, kemungkinan besar tidak karena programmer malas dengan hal-hal yang bersifat repetitif, termasuk saya. Oleh karena itu, sebisa mungkin, kita ingin mengotomasi hal tersebut dan disitulah CI/CD to the rescue.

CI/CD

Apa itu CI/CD? CI/CD merupakan singkatan dari continuous integration and continuous delivery/deployment. Continuous integration sendiri merupakan suatu praktik untuk mengintegrasikan perubahan kode secara otomatis dan berkala ke repositori kode, seperti GitHub repository ataupun GitLab repository. Di sisi lain, continuous delivery/deployment berkaitan dengan penyebaran aplikasi secara otomatis. Lalu, apa hubungannya CI/CD dengan TDD? Banyak.

Ketika kita mengirim (push) kode kita ke remote repository, dengan adanya continuous integration, kode kita dapat di-build dan di-testing secara otomatis. Dengan demikian, kita dapat memastikan bahwa kode yang kita kirim ke remote repository berjalan sebagaimana mestinya. Hal ini untuk menghindari “kodenya di mesin saya jalan kok, tapi di production malah error”. Berikut adalah contoh tampilan tahap testing pada CI/CD.

Tampilan Tahap Testing pada GitLab

Perhatikan pada gambar di atas, test dan code coverage dijalankan secara otomatis pada mesin (virtual machine) yang disediakan. Hal tersebut tentu akan sangat membantu untuk menjaga kualitas kode dan memastikan test berjalan dengan baik pada lingkungan yang hampir mendekati lingkungan produksi. Apakah sampai di sini cukup? Biasanya, apabila aplikasi yang kita buat merupakan aplikasi dengan skala yang cukup besar, kita akan memanfaatkan juga monitoring tools (seperti SonarCloud) untuk mengecek kode kita, termasuk coverage. Berikut adalah tampilan coverage dari monitoring tools SonarCloud.

Coverage pada SonarCloud

Jika kita lihat pada gambar di atas, tentu tampilan yang disediakan oleh SonarCloud lebih mudah untuk diakses dibandingkan dengan command line interface pada CI/CD GitLab. Bahkan, pada SonarCloud kita bisa mengecek bagian mana yang tidak ter-cover oleh test yang kita buat. Tentunya hal ini cukup sulit jika kita lakukan dengan command line interface pada CI/CD GitLab.

Line Coverage pada SonarCloud

Dengan dikatakan itu, kita tentu mengetahui bahwa banyak hal yang harus dipersiapkan di awal dalam pengembangan aplikasi kita. Namun, percayalah manfaatnya lebih besar untuk aplikasi kalian ke depannya dibandingkan dengan pengorbanan kalian di awal (“bersakit sakit dahulu bersenang senang kemudian”).

Trend TDD Saat Ini

Apakah ada perkembangan dalam penerapan TDD sejak pertama kali diperkenalkan oleh Kent Beck pada tahun 2003? Ya, salah satu perkembangan penerapan TDD saat ini yang paling saya rasakan adalah pengguna kecerdasan buatan (artificial intelligence) untuk membantu dalam pembuatan unit test. Secara sederhana, kecerdasan buatan dapat membantu kita dalam membuat unit test yang baik.

Nah, sebagai mahasiswa Ilmu Komputer, tentu saja saya tertarik terhadap teknologi terbaru ini (semoga bisa meringankan tugas saya, haha). Namun, hal yang perlu diperhatikan adalah unit test yang sesuai hanya bisa di-generate oleh AI tools ketika kode implementasi sudah tersedia. Hal ini berarti kita akan sulit menerapkan TDD jika hanya mengandalkan AI tools tersebut untuk menghasilkan unit test yang sesuai. Sebagai gambaran, saya akan mencoba berbagai AI tools untuk membuat unit test dari kode fungsi divide() yang telah dibuat sebelumnya.

ChatGPT

ChatGPT sebagai general purpose AI dapat kita gunakan untuk membantuk pembuatan unit test. Berikut adalah hasil unit test yang di-generate oleh ChatGPT.

Unit test yang dihasilkan oleh ChatGPT

Jika kita menjalankan test-nya, kita akan mendapatkan output sebagai berikut.

 FAIL  src/divide.test.js
ChatGPT
√ should divide two numbers correctly (3 ms)
√ should return null if divisor is 0
√ should return Infinity if dividend is positive and divisor is 0 (1 ms)
√ should return -Infinity if dividend is negative and divisor is 0 (1 ms)
× should return 0 if both dividend and divisor are 0 (1 ms)
× should return Infinity if dividend is positive and divisor is -0 (2 ms)
× should return -Infinity if dividend is negative and divisor is -0

Apakah unit test yang dihasilkan akurat? Tidak.

Github Copilot

Berdasarkan Stack Overflow 2023 Developer Survey, Github Copilot merupakan AI developers tools yang digunakan sekitar 55% responden dari survey tersebut. Github Copilot sendiri adalah AI-powered code completion tool yang dikembangkan oleh Github dengan berkolaborasi dengan OpenAI (pembuat ChatGPT). Lalu, apa bedanya dengan chatGPT? Secara sederhana, Github Copilot dedesain spesifik untuk tugas yang berkaitan dengan code generation yang dilatih dengan kumpulan kode dari repositori publik di GitHub. Selain itu, Github Copilot dapat memberikan saran berdasarkan kode aktif yang kita tulis atau edit di dalam text editor. Dengan itu dikatakan, Berikut adalah hasil unit test yang di-generate oleh Github Copilot.

Prompt untuk menghasilkan Unit test dengan Github Copilot
describe("Github Copilot", () => {
test("should return 0 when dividing 0 by any number", () => {
expect(divide(0, 10)).toBe(0);
});

test("should return the dividend when dividing any number by 1", () => {
expect(divide(10, 1)).toBe(10);
expect(divide(-5, 1)).toBe(-5);
expect(divide(0, 1)).toBe(0);
});

test("should return a decimal number when dividing two numbers with a remainder", () => {
expect(divide(5, 2)).toBe(2.5);
expect(divide(7, 3)).toBeCloseTo(2.333, 3);
expect(divide(10, 3)).toBeCloseTo(3.333, 3);
});

it("should return the correct quotient when dividing two numbers", () => {
expect(divide(10, 2)).toBe(5);
expect(divide(15, 3)).toBe(5);
expect(divide(8, 4)).toBe(2);
});

it("should return null when dividing by zero", () => {
expect(divide(10, 0)).toBeNull();
expect(divide(15, 0)).toBeNull();
expect(divide(8, 0)).toBeNull();
});
});

Jika kita menjalankan test-nya, kita akan mendapatkan output sebagai berikut.

 PASS src/divide.test.js
Github Copilot
√ should return 0 when dividing 0 by any number
√ should return the dividend when dividing any number by 1
√ should return a decimal number when dividing two numbers with a remainder (1 ms)
√ should return the correct quotient when dividing two numbers (1 ms)
√ should return null when dividing by zero (1 ms)

Berdasarkan percobaan di atas, Github Copilot menghasilkan unit test yang lebih baik dibandingkan dengan ChatGPT dengan berbagai testcase yang diberikan.

CodiumAI

CodiumAI adalah AI tools yang dapat digunakan juga untuk membuat test. Lalu, apa perbedaannya dengan ChatGPT dan Github Copilot yang telah kita bahas sebelumnya? Mari, kita simak FAQ pada laman CodiumAI.

Unlike general-purpose code completion or generation tools, CodiumAI focuses on code integrity: generating tests that help you understand how your code behaves, finding edge cases and suspicious behaviors, and making your code more robust. CodiumAI is not just another fancy ‘Language-model-API in your IDE’ because:

a. We’re professionals in the testing-domain prompting.
b. We parallelize and chain multiple prompts to create a unique variety of meaningful tests.
c. We efficiently gather a broad code context for the prompts.
d. We allow you to interact with each test separately.

- CodiumAI on Why don’t I just ask ChatGPT/Copilot to write my tests? How is CodiumAI different?

Dengan itu dikatakan, mari kita coba. Berikut adalah hasil unit test yang di-generate oleh CodiumAI.

Unit test yang dihasilkan oleh CodiumAI

Menurut saya, CodiumAI menghasilkan unit test yang baik dengan berbagai penjelasan yang diberikan. Selain itu, CodiumAI juga memberikan saran terkait test yang dibuat untuk mencakup berbagai kemungkinan sebagai berikut.

Happy Path Behaviour coverage dari fungsi divide()
Edge Cases Behaviour coverage dari fungsi divide()

Bahkan, kode fungsi divide() yang kita buat dapat dijelaskan dengan baik oleh CodiumAI.

Unit test untuk Component LoginForm?

Bagaimana jika Github Copilot atau CodiumAI digunakan untuk menguji component LoginForm()pada bagian Implementasi TDD pada Dunia Nyata di atas? Tidak bisa. Hal ini dikarenakan Github Copilot ataupun CodiumAI tidak bisa menerima semua konteks terkait kode aplikasi yang kita punya, seperti tipe data pada TypeScript, library yang kita gunakan, dan testing framework yang digunakan. Selain itu, karena AI tools dilatih menggunakan data kode yang sudah ada, bagaimana AI tools bisa membuat test untuk LoginForm() yang menggunakan berbagai fungsi (hook) terbaru yang berkaitan dengan teknologi terbaru (React Server Component) dengan tepat tanpa ada data yang cukup?

Kesimpulan

Melalui blog ini, kita telah memahami sedikit terkait dengan TDD yang merupakan pengembangan perangkat lunak yang mengedepankan pembuatan test terlebih dahulu sebelum kode produksi. Selain itu, kita mengetahui seberapa penting TDD ini, berbagai keuntungan dari TDD, Implementasi TDD, penerapan trend TDD saat ini. Semoga blog ini bermanfaat!

Sumber Referensi

  1. Martin, R. C. (2009). Clean code: A Handbook of Agile Software Craftsmanship. Prentice Hall.

2. Beck, K. (2015). Test-driven development by example. Addison-Wesley.

3. Mocking is a Code Smell by Eric Elliott

4. Return Early Pattern by Leonel Menaia Dev

5. What is CI/CD by Red Hat

--

--