Pendahuluan

Pernah merasa kalau kita berusaha sekuat mungkin untuk membuat skor code coverage dari code program mencapai 100% ?

kalau iya, sekarang muncul pertanyaan :

  • Apakah tujuannya untuk mencapai persyaratan Code Coverage yang sudah ditentukan dari tim atau management, atau standar CI/CD atau biar bisa langsung diapprove kalau merge request/pull request manual atau otomatis ?
  • Atau untuk kebanggaan tersendiri kalau code kita punya code coverage tinggi ?
  • Atau ingin mengecek ketangguhan code kita terhadap berbagai macam kemungkinan kasus ?

Kalau jawaban Anda adalah yang pertama dan kedua, yaitu untuk mencapai persyaratan Code Coverage atau untuk kebanggaan/prestasi, maka Anda dan Saya sudah terjebak dalam kesalahan pemahaman tentangunit testing dan code coverage.


Memangnya ada yang salah dengan code coverage yang tinggi ?

Tidak, tentu saja tidak. Code coverage yang tinggi tentu saja bukan hal yang salah. Secara logika, Code coverage yang tinggi tentu saja mencerminkan seberapa banyak code kita yang berhasil dites fungsionalitasnya.

Atau seberapa berusahanya Anda mengecek line code mana yang masih belum di test dengan unit testing.

Tapi secara filosofis, Code Coverage ini tidak otomatis mencerminkan seberapa bagus program yang kita buat, apakah bug free , apakah memenuhi requirement yang dibutuhkan, atau apakah mengikuti prinsip software engineering.


Lalu apa dong kegunaan dari code coverage ini ?

Sekali lagi fungsi utamanya Code Coverage adalah sebagai ukuran seberapa banyak bagian dari code kita yang sudah ditest.

Code Coverage ini hanyalah salah satu alat saja untuk menemukan bug tersembunyi yang mungkin saja luput dari perhatian kita ketika membuat code program.

Tapi sekali lagi juga, bukan berarti code coverage yang tinggi berarti code/program kita bebas bug.


Contohnya bagaimana ?

Misalkan kita punya code untuk perkalian :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.example.demo;

import org.springframework.stereotype.Component;

@Component
public class Perhitungan {

	public int perkalian(int angka1, int angka2) {
		int hasil = angka1 * angka2;
		return hasil;
	}
}
Program sederhana yang melakukan perkalian dari 2 angka integer.

Dan tentunya kita membuat sebuah unit testing sbb :

 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
package com.example.demo;

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

import org.junit.Before;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class PerhitunganTest {

	@InjectMocks
	private Perhitungan perhitungan = new Perhitungan();

	@Before
	public void init() {
		MockitoAnnotations.openMocks(this);
	}

	@Test
	public void tesPerkalian_Sederhana() {
		int hasil = perhitungan.perkalian(2, 3);

		// assert
		assertEquals(6, hasil);
	}

	@Test
	public void tesPerkalian_MaxInteger() {
		int hasil = perhitungan.perkalian(Integer.MAX_VALUE, Integer.MAX_VALUE);

		// assert
		assertEquals(Integer.MAX_VALUE * Integer.MAX_VALUE, hasil);
	}
}
Kita membuat unit test sederhana juga yang melakukan testing perkalian 2 angka integer.

Dan Code Coverage untuk file Perhitungan adalah 100%.

Cukup mengesankan ya ?

Bikin satu unit test, kemudian jalankan, dan mendapatkan code coverage 100%.

Tapi tunggu dulu, ada test case yang sepertinya bermasalah. yaitu di fungsi tesPerkalian_MaxInteger()

Permasalahannya Dimana ?

  • perkalian Integer.MAX_VALUE * Integer.MAX_VALUE, seharusnya hasilnya tidak akan muat/fit dengan integer. Thats is simple logic. Nilai maksimum dikali nilai maksimum tidak mungkin muat dan harusnya ada error/Exception atau disimpan ke tipe data yang lebih besar nilainya seperti long. Akan tetapi di tesPerkalian_MaxInteger() tetap saja berjalan tanpa ada error atau exception.

  • assertion untuk Integer.MAX_VALUE * Integer.MAX_VALUE yang kembali menggunakan fungsi yang sama, tidak mengetes fungsi yang sebenarnya. Unit test adalah testing yang kita lakukan dengan ekspektasi nilai yang kita definisikan, bukan dengan memanggil fungsi yang sama dengan code yang kita test.

  • kalau kita coba melakukan perkalian Integer.MAX_VALUE * Integer.MAX_VALUE maka hasilnya adalah angka 1. Hmm, tidak kita sangka-sangka kan ternyata Java melakukan perkalian seperti itu.

hmm..kalau begitu ada kemungkinan bug di code kita diatas.

Dari sini kita bisa melihat kalau Code Coverage yang tinggi belum tentu membuat code kita bebas bug.

Akan tetapi lebih kepada pengetesan kondisi by kemungkinan kasus yang akan membantu kita untuk mengetes dan memperbaiki bug yang ada di program kita.

Trus, apa yang mesti kita lakukan dengan code program yang tadi.

Dengan melakukan unit test Integer.MAX_VALUE * Integer.MAX_VALUE, maka kita bisa mendapatkan kemungkinan bug yang ada.

Sehingga yang kita lakukan adalah mengubah code/program kita misalnya dengan expect Exception kalau hasil perkaliannya melewati Integer.MAX_VALUE, misalnya dengan memakai bantuan method static Math.multiplyExact.

Bisa jadi kita ubah code nya sbb :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.example.demo;

import org.springframework.stereotype.Component;

@Component
public class Perhitungan {

	public int perkalian(int angka1, int angka2) {
		int hasil = Math.multiplyExact(angka1, angka2);
		return hasil;
	}
}

Dan tentunya unit testnya kita ubah, dengan melakukan expect exception , sbb :

 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
package com.example.demo;

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

import org.junit.Before;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class PerhitunganTest {

	@InjectMocks
	private Perhitungan perhitungan = new Perhitungan();

	@Before
	public void init() {
		MockitoAnnotations.openMocks(this);
	}

	@Test
	public void tesPerkalian_Sederhana() {
		int hasil = perhitungan.perkalian(2, 3);

		// assert
		assertEquals(6, hasil);
	}

	@Test
	public void tesPerkalian_MaxInteger() {
		Assertions.assertThrows(ArithmeticException.class, () -> {
			perhitungan.perkalian(Integer.MAX_VALUE, Integer.MAX_VALUE);
		});
	}
}

Cool, akhirnya kita bisa melihat bahwa code kita yang pertama tidak bebas dari bug, dan kita bisa menemukannya melalui unit test yang cukup.

Kenapa kita bisa menemukannya ?

Karena group band NAFF menyanyikannya dulu.. ups salah bro..maap..maap..

Kita bisa menemukan bug tersebut karena kita membuat unit testing terhadap kemungkinan kondisi yang ada, bukan membuat unit test hanya untuk mencapai code coverage yang tinggi.

Thats the philosophy…!!!

Jadi kesimpulannya

Kesimpulannya :

  • Jangan terpaku dengan code coverage yang tinggi, tapi buatlah unit test dengan kemungkinan kondisi yang sesuai dengan requirement dan kemungkinan masukan/input yang berinteraksi dengan program kita.

  • Tentukan nilai bawah, nilai normal, dan nilai atas dari kondisi dari logika code program kita. Contohnya diatas seperti menentukan Integer.MAX_VALUE, integer normal, dan Integer.MIN_VALUE untuk melakukan testing.

  • Jangan sungkan untuk merefactor code/program kita kalau ternyata code/program kita tidak merepresentasikan requirement yang kita butuhkan. Jangan salahkan unit testnya melulu :), atau melakukan manipulasi unit test agar mencapai code coverage yang tinggi.