Pengujian unit

Pengujian unit dengan AUnit dan kasus uji berbasis kontrak.

Daftar isi

Pengujian unit (unit testing) adalah kegiatan menguji unit terkecil sebuah program, yaitu subprogram, secara terpisah dari program utamanya, dengan hasil yang diperiksa oleh program pengujian itu sendiri. Pada Ada, pustaka yang lazim digunakan untuk hal ini adalah AUnit, yang menyediakan tipe test case, test suite, dan reporter untuk menyusun serta menjalankan pemeriksaan otomatis. Topik ini membangun pengujian untuk satu unit nyata, yaitu fungsi pembagian yang dilengkapi kontrak pada package Kalkulator_Ops, mulai dari test case pola klasik, suite dan runner, berkas proyek yang mengimpor pustaka AUnit, hingga versi kasus uji yang sengaja salah untuk melihat laporan FAIL. Setelah topik ini dibaca, program pengujian dapat dibangun dengan satu perintah gprbuild dan setiap baris laporannya dapat dibaca.

Unit yang diuji#

Unit yang diuji pada topik ini adalah package Kalkulator_Ops yang memuat satu fungsi, yaitu Bagi. Fungsi Bagi pada package ini identik dengan Bagi pada proyek kalkulator di topik tentang berkas proyek, tetapi fungsi Tambah tidak disertakan karena tidak menjadi sasaran pengujian. Kontrak pada fungsi Bagi menentukan isi test case, sebab aspek Pre menetapkan pemanggilan yang sah dan aspek Post menetapkan sifat hasil yang wajib dipenuhi.

ada kalkulator_ops.ads
package Kalkulator_Ops is

   --  Kontrak: penyebut tidak boleh nol, dan hasilnya mengikuti
   --  sifat pembagian bilangan bulat.
   function Bagi (Pembilang, Penyebut : in Integer) return Integer with
     Pre  => Penyebut /= 0,
     Post => Bagi'Result * Penyebut + Pembilang rem Penyebut = Pembilang;

end Kalkulator_Ops;
ada kalkulator_ops.adb
package body Kalkulator_Ops is

   function Bagi (Pembilang, Penyebut : in Integer) return Integer is
   begin
      return Pembilang / Penyebut;
   end Bagi;

end Kalkulator_Ops;

Menguji Bagi berarti menargetkan tiga perilaku yang dinyatakan oleh kontraknya. Sifat Post diuji dengan pasangan bilangan biasa, yaitu memastikan hasil bagi dikali penyebut ditambah sisa mengembalikan pembilang semula. Batas terkecil diuji dengan penyebut 1, yaitu nilai sah terkecil di atas nol yang diperbolehkan kontrak. Pelanggaran Pre diuji dengan penyebut 0, yaitu pemanggilan yang wajib memunculkan Ada.Assertions.Assertion_Error ketika kontrak diperiksa. Badan fungsi Bagi hanya berisi satu operasi pembagian, sehingga pemeriksaan yang berarti justru terletak pada sifat yang dijanjikan kontraknya, bukan pada isi badan fungsi. Tiga perilaku inilah yang menjadi tiga rutin uji pada bagian berikutnya.

Test case pola klasik#

AUnit menyediakan tipe abstrak AUnit.Test_Cases.Test_Case sebagai induk semua test case. Pola klasik menurunkan tipe tersebut menjadi tipe baru tanpa komponen data tambahan, sebagaimana baris type Tes_Bagi is new AUnit.Test_Cases.Test_Case with null record; pada spesifikasi berikut. Dua operasi abstrak pada tipe induk wajib ditulis ulang (override), yaitu Register_Tests yang mendaftarkan rutin uji dan Name yang memberi nama test case. Setiap rutin uji berprofil procedure ... (T : in out AUnit.Test_Cases.Test_Case'Class) dan tidak mengembalikan nilai.

ada kalkulator_tes.ads
with AUnit;
with AUnit.Test_Cases;

package Kalkulator_Tes is

   type Tes_Bagi is new AUnit.Test_Cases.Test_Case with null record;

   procedure Register_Tests (T : in out Tes_Bagi);
   function Name (T : Tes_Bagi) return AUnit.Message_String;

   --  Rutin pengujian:
   procedure Tes_Post_Pembagian (T : in out AUnit.Test_Cases.Test_Case'Class);
   procedure Tes_Batas_Penyebut (T : in out AUnit.Test_Cases.Test_Case'Class);
   procedure Tes_Pre_Dilanggar  (T : in out AUnit.Test_Cases.Test_Case'Class);

end Kalkulator_Tes;

Badan package mengisi kedua operasi tersebut dan memuat tiga rutin uji. Pendaftaran dilakukan oleh Register_Tests melalui Register_Routine dari AUnit.Test_Cases.Registration, dengan argumen berupa test case, akses ke prosedur uji seperti Tes_Post_Pembagian'Access, dan label yang muncul pada laporan. Nama test case dikembalikan oleh Name melalui AUnit.Format. Assertion ditulis dengan Assert dari AUnit.Assertions, yaitu pemanggilan yang menerima ekspresi Boolean dan pesan yang tampil apabila ekspresi bernilai salah.

ada kalkulator_tes.adb
with Ada.Assertions;
with AUnit.Assertions;
with Kalkulator_Ops;

package body Kalkulator_Tes is

   use AUnit.Assertions;

   --  Kasus uji Post: hasil kali penyebut ditambah sisa mengembalikan
   --  pembilang.
   procedure Tes_Post_Pembagian
     (T : in out AUnit.Test_Cases.Test_Case'Class)
   is
      pragma Unreferenced (T);
      Hasil : constant Integer := Kalkulator_Ops.Bagi (100, 8);
   begin
      Assert (Hasil * 8 + 100 rem 8 = 100, "Sifat Post tidak terpenuhi");
   end Tes_Post_Pembagian;

   --  Kasus uji batas: penyebut terkecil yang sah adalah 1.
   procedure Tes_Batas_Penyebut
     (T : in out AUnit.Test_Cases.Test_Case'Class)
   is
      pragma Unreferenced (T);
   begin
      Assert (Kalkulator_Ops.Bagi (7, 1) = 7, "Pembagian dengan satu salah");
   end Tes_Batas_Penyebut;

   --  Kasus uji Pre: penyebut nol melanggar kontrak dan memunculkan
   --  Assertion_Error.
   procedure Tes_Pre_Dilanggar
     (T : in out AUnit.Test_Cases.Test_Case'Class)
   is
      pragma Unreferenced (T);
      Hasil : Integer;
      pragma Unreferenced (Hasil);
   begin
      Hasil := Kalkulator_Ops.Bagi (7, 0);
      Assert (False, "Pelanggaran Pre tidak terdeteksi");
   exception
      when Ada.Assertions.Assertion_Error =>
         null;
   end Tes_Pre_Dilanggar;

   procedure Register_Tests (T : in out Tes_Bagi) is
      use AUnit.Test_Cases.Registration;
   begin
      Register_Routine (T, Tes_Post_Pembagian'Access, "Post pembagian");
      Register_Routine (T, Tes_Batas_Penyebut'Access, "Batas penyebut");
      Register_Routine (T, Tes_Pre_Dilanggar'Access, "Pre dilanggar");
   end Register_Tests;

   function Name (T : Tes_Bagi) return AUnit.Message_String is
      pragma Unreferenced (T);
   begin
      return AUnit.Format ("Kasus uji Kalkulator_Ops");
   end Name;

end Kalkulator_Tes;

Rutin Tes_Pre_Dilanggar memperlihatkan pola yang lazim untuk kontrak, yaitu pemanggilan yang sengaja melanggar Pre lalu menangkap Assertion_Error yang dimunculkan oleh pelanggaran tersebut. Badan prosedur memanggil Bagi (7, 0), sedangkan baris Assert (False, ...) hanya tercapai apabila Assertion_Error tidak muncul, sehingga kegagalan test case menandakan kontrak tidak diperiksa. Parameter T pada setiap rutin tidak digunakan, dan pragma Unreferenced menginstruksikan kompilator untuk tidak memberi peringatan atas parameter tersebut.

Suite dan runner#

Suite menghimpun beberapa test case agar seluruhnya dijalankan dalam satu sesi. Tipe AUnit.Test_Suites.Test_Suite mewakili himpunan tersebut, sedangkan AUnit.Test_Suites.Access_Test_Suite menjadi tipe kembalian fungsi yang membangun suite tersebut. Fungsi Suite pada program berikut mengalokasikan suite dengan new Test_Suite lalu memasukkan satu test case melalui Add_Test.

ada tes_utama.adb
with AUnit.Run;
with AUnit.Reporter.Text;
with AUnit.Test_Suites;
with Kalkulator_Tes;

procedure Tes_Utama is

   use AUnit.Test_Suites;

   function Suite return Access_Test_Suite is
      S : constant Access_Test_Suite := new Test_Suite;
   begin
      Add_Test (S, new Kalkulator_Tes.Tes_Bagi);
      return S;
   end Suite;

   procedure Jalankan is new AUnit.Run.Test_Runner (Suite);

   Reporter : AUnit.Reporter.Text.Text_Reporter;

begin
   Jalankan (Reporter);
end Tes_Utama;

Runner dihasilkan dengan menginstansiasi generik AUnit.Run.Test_Runner terhadap fungsi suite, yaitu procedure Jalankan is new AUnit.Run.Test_Runner (Suite);. Nama prosedur hasil instansiasi bebas dipilih, tetapi nama fungsi pada instansiasi wajib sama dengan nama fungsi yang membangun suite. Tipe kembalian fungsi Suite berupa akses karena suite dialokasikan secara dinamis dengan new, sedangkan reporter cukup dibuat sebagai objek biasa pada bagian deklaratif prosedur utama. Prosedur Jalankan dipanggil dengan satu argumen berupa reporter, dan AUnit.Reporter.Text.Text_Reporter mencetak laporan berformat teks ke terminal. Test case tambahan cukup dipasang dengan Add_Test lagi pada fungsi yang sama, dan suite juga dapat menghimpun suite lain, sehingga pengujian besar tetap dijalankan sebagai satu kesatuan. Pola suite dan runner ini mengikuti contoh resmi pada AUnit Cookbook.

Proyek pengujian#

Pembangunan program pengujian menggunakan berkas proyek tes.gpr yang mengimpor pustaka AUnit melalui with "aunit";. Proyek pustaka bernama aunit ditemukan oleh gprbuild pada jalur pencarian proyek yang diatur melalui GPR_PROJECT_PATH, sehingga blok berikut mengekspornya menuju direktori lib/gnat dari unduhan AUnit sebelum memanggil gprbuild. Jalur pada contoh mengikuti letak unduhan pada lingkungan penulisan modul ini, dan penyesuaiannya dilakukan sesuai letak unduhan masing-masing. Atribut Main memuat dua berkas utama, yaitu tes_utama.adb dan tes_gagal.adb, sehingga satu perintah pembangunan menghasilkan dua program pengujian.

gpr tes.gpr
with "aunit";

project Tes is

   for Source_Dirs use (".");
   for Object_Dir use "obj";
   for Exec_Dir use "bin";
   for Main use ("tes_utama.adb", "tes_gagal.adb");

   package Compiler is
      for Default_Switches ("Ada") use ("-gnata");
   end Compiler;

end Tes;

Switch -gnata pada package Compiler mengaktifkan pemeriksaan assertion dan kontrak saat run time. Pemeriksaan ini wajib untuk proyek pengujian di atas, karena rutin Tes_Pre_Dilanggar mengandalkan pelanggaran Pre yang hanya memunculkan exception apabila kontrak diperiksa. Pembangunan dan peluncuran program pengujian pertama menggunakan dua perintah berikut.

bash
# arahkan gprbuild ke folder proyek pustaka AUnit
export GPR_PROJECT_PATH="$HOME/alire-crate/aunit/lib/gnat"
# bangun kedua program pengujian dari direktori bundle
gprbuild -q -P tes.gpr
# jalankan program pengujian pertama
./bin/tes_utama
text Keluaran

OK Kasus uji Kalkulator_Ops : Post pembagian
OK Kasus uji Kalkulator_Ops : Batas penyebut
OK Kasus uji Kalkulator_Ops : Pre dilanggar

Total Tests Run:   3
Successful Tests:  3
Failed Assertions: 0
Unexpected Errors: 0

Laporan Text_Reporter menuliskan satu baris untuk setiap rutin uji, diawali status dan diikuti nama test case serta label rutin. Ringkasan di bawah baris-baris tersebut memuat empat angka, yaitu jumlah pengujian yang dijalankan, jumlah yang berhasil, jumlah assertion yang gagal, dan jumlah exception tak terduga. Keluaran di atas menunjukkan ketiga rutin berhasil, sehingga dua baris terakhir bernilai nol.

Laporan pengujian yang gagal#

Kasus uji yang salah dibuat untuk melihat bentuk laporan FAIL. Package Kalkulator_Tes_Gagal memuat satu rutin yang pemeriksaannya tidak menambahkan sisa pembagian, sehingga ekspresi Hasil * 8 = 100 bernilai salah karena 12 dikali 8 menghasilkan 96. Struktur package tersebut sama dengan test case sebelumnya, yaitu menurunkan Test_Case, mengoverride Register_Tests dan Name, lalu mendaftarkan satu rutin.

ada kalkulator_tes_gagal.ads
with AUnit;
with AUnit.Test_Cases;

package Kalkulator_Tes_Gagal is

   type Tes_Bagi_Gagal is new AUnit.Test_Cases.Test_Case with null record;

   procedure Register_Tests (T : in out Tes_Bagi_Gagal);
   function Name (T : Tes_Bagi_Gagal) return AUnit.Message_String;

   --  Rutin pengujian:
   procedure Tes_Post_Salah (T : in out AUnit.Test_Cases.Test_Case'Class);

end Kalkulator_Tes_Gagal;
ada kalkulator_tes_gagal.adb
with AUnit.Assertions;
with Kalkulator_Ops;

package body Kalkulator_Tes_Gagal is

   use AUnit.Assertions;

   --  Kasus uji yang lupa menambahkan sisa pembagian.
   procedure Tes_Post_Salah (T : in out AUnit.Test_Cases.Test_Case'Class) is
      pragma Unreferenced (T);
      Hasil : constant Integer := Kalkulator_Ops.Bagi (100, 8);
   begin
      Assert (Hasil * 8 = 100, "Sifat Post tidak terpenuhi");
   end Tes_Post_Salah;

   procedure Register_Tests (T : in out Tes_Bagi_Gagal) is
      use AUnit.Test_Cases.Registration;
   begin
      Register_Routine (T, Tes_Post_Salah'Access, "Post pembagian");
   end Register_Tests;

   function Name (T : Tes_Bagi_Gagal) return AUnit.Message_String is
      pragma Unreferenced (T);
   begin
      return AUnit.Format ("Kasus uji yang salah");
   end Name;

end Kalkulator_Tes_Gagal;
ada tes_gagal.adb
with AUnit.Run;
with AUnit.Reporter.Text;
with AUnit.Test_Suites;
with Kalkulator_Tes_Gagal;

procedure Tes_Gagal is

   use AUnit.Test_Suites;

   function Suite return Access_Test_Suite is
      S : constant Access_Test_Suite := new Test_Suite;
   begin
      Add_Test (S, new Kalkulator_Tes_Gagal.Tes_Bagi_Gagal);
      return S;
   end Suite;

   procedure Jalankan is new AUnit.Run.Test_Runner (Suite);

   Reporter : AUnit.Reporter.Text.Text_Reporter;

begin
   Jalankan (Reporter);
end Tes_Gagal;

Program pengujian kedua dibangun oleh berkas proyek yang sama, sehingga perintah pembangunan cukup diulang pada sesi terminal yang sama sebelum program diluncurkan.

bash
# pastikan program pengujian telah terbangun
gprbuild -q -P tes.gpr
# jalankan program pengujian dengan kasus uji yang salah
./bin/tes_gagal
text Keluaran

FAIL Kasus uji yang salah : Post pembagian
    Sifat Post tidak terpenuhi
    at kalkulator_tes_gagal.adb:13

Total Tests Run:   1
Successful Tests:  0
Failed Assertions: 1
Unexpected Errors: 0

Baris FAIL memuat nama test case dan label rutin yang gagal, lalu dua baris menjorok memuat pesan Assert dan letak berkas beserta nomor barisnya. Ringkasan mencatat Failed Assertions: 1, sehingga jumlah pengujian yang berhasil turun menjadi nol. Pelaporan ini memperlihatkan cara kerja AUnit, yaitu kegagalan dilaporkan sebagai baris pada laporan yang sama dengan ringkasannya, alih-alih menghentikan program pengujian.

Kesalahan umum#

Kesalahan pertama adalah mendefinisikan rutin uji tetapi tidak mendaftarkannya pada Register_Tests. Rutin yang tidak didaftarkan tidak pernah dijalankan, sehingga laporan menampilkan jumlah pengujian yang lebih kecil daripada yang diperkirakan dan cacat pada rutin tersebut tidak terdeteksi. Gejalanya tidak mencolok, misalnya Total Tests Run: 2 padahal tiga rutin sudah ditulis, karena pembangunan tetap berhasil dan laporan tetap memuat status OK pada rutin-rutin lain. Perbaikannya, pastikan setiap prosedur uji memiliki satu baris Register_Routine yang sesuai.

Kesalahan kedua adalah menghapus switch -gnata dari berkas proyek pengujian. Tanpa switch tersebut, pemeriksaan kontrak dilewati saat run time, sehingga pemanggilan Bagi (7, 0) tidak memunculkan Assertion_Error melainkan berlanjut ke pembagian bulat dengan nol dan memunculkan Constraint_Error. Handler yang menangkap Assertion_Error tidak mencocokkan Constraint_Error tersebut, sehingga AUnit melaporkan rutin Pre dilanggar dengan status ERROR dan ringkasan Unexpected Errors: 1. Pesan Exception Message: kalkulator_ops.adb:5 divide by zero pada laporan menunjuk badan fungsi yang diuji, bukan baris Assert, sehingga penyebabnya mudah disangka cacat pada fungsi yang diuji. Perbaikannya, pertahankan -gnata pada proyek pengujian yang menyasar kontrak.

Ringkasan#

  • Test case AUnit pola klasik menurunkan AUnit.Test_Cases.Test_Case dengan null record, lalu mengoverride Register_Tests untuk mendaftarkan rutin uji dan Name untuk menamainya.
  • Fungsi suite menghimpun test case dengan Add_Test, runner dihasilkan dari instansiasi AUnit.Run.Test_Runner, dan Text_Reporter mencetak status per rutin beserta ringkasannya.
  • Proyek pengujian mengimpor pustaka dengan with "aunit", dan switch -gnata wajib dipertahankan agar pelanggaran kontrak tetap terdeteksi.

Referensi#