Memakai PHPUnit Di Zend Studio


Seorang mahasiswa menjalankan programnya, men-klik beberapa kali, lalu muncul pesan kesalahan. Ia kemudian mencoba memperbaiki kode programnya. Setelah itu, ia kembali lagi menjalankan programnya, men-klik beberapa kali, ternyata pesan kesalahan yang sama masih muncul. Ia kembali melakukan perubahan pada kode programnya. Siklus ini berlangsung terus menerus hingga sang mahasiswa tidak menemukan pesan kesalahan lagi. Ini adalah salah satu contoh melakukan pengujian secara manual.

Teknik pengujian yang lebih otomatis adalah dengan menggunakan unit testing dimana seorang programmer menguji kebenaran kode programnya melalui kode program. Keuntungannya? Proses pengujian bisa dilakukan secara berulang kali tanpa melelahkan si programmer. Bahkan, pada metodologi Test-driven Development (TDD), melakukan unit testing adalah hal yang wajib karena pengujian menjadi pemicu pengembangan kode program! Test-driven Development (TDD) adalah salah satu konsep dalam Extreme Programming (XP). XP sendiri adalah metode pengembangan software yang mengikuti nilai dan prinsip dalam Agile Manifesto (baca: Apa itu Agile Manifesto?).

Bila unit testing di Java dilakukan dengan menggunakan JUnit, maka pada PHP, saya dapat menggunakan PHPUnit. Beruntungnya, Zend Studio 10.5 dilengkapi dengan integrasi PHPUnit sehingga saya tidak perlu men-download PHPUnit secara manual.

Unit testing sebaiknya dilakukan per unit eksekusi terpisah yaitu class. Setiap class memiliki test case tersendiri yang berdiri sendiri. Biasanya yang saya uji adalah kode program di domain model. Pengujian pada controller dapat dianggap sebagai integration testing karena harus melibatkan request HTTP, database dan beberapa class berbeda. Sementara itu, pengujian pada view merupakan front-end testing yang dilakukan dengan memakai tool sejenis Selenium yang berjalan di browser.

Sebagai contoh, pada sebuah proyek terdapat domain model pada file domain.php seperti berikut ini:

<?php

/**
 * Class ini dipakai untuk mewakili sebuah penjualan.
 *
 * @author Solid Snake
 *
 */
class Penjualan {

   /**
    * Nomor untuk faktur penjualan ini.
    * 
    * @var string
    */
   public $nomor;

   /**
    * Tanggal yang terterak di faktur.
    * 
    * @var string   
    */
   public $tanggal;

   /**
    * Array yang berisi satu atau lebih `Item` untuk faktur ini.
    * 
    * @var Item[]
    */
   public $arrItem;

   /**
    * Diskon untuk penjualan ini.
    *  
    * @var Diskon
    */
   public $diskon;

   /**
    * 
    * @param string $nomor
    * @param date $tanggal
    */
   public function __construct($nomor, $tanggal, $diskon = NULL) {
       $this->nomor = $nomor;
       $this->tanggal = $tanggal;
       $this->arrItem = [];
       $this->diskon = $diskon?: new Diskon();
   }

   /**
    * Menambah `Item` baru untuk penjualan ini.
    * 
    * @param Item $item
    */
   public function tambahItem($item) {
       $this->arrItem[] = $item;
   }

   /**
    * Mengembalikan total untuk transaksi ini.
    * 
    * @return double
    */
   public function getTotal() {
      $total = 0;
      foreach ($this->arrItem as $item) {
           $total += $item->getTotal();
      }
      return $this->diskon->setelahDiskon($total);      
   }       

}

/**
 * Class ini dipakai untuk mewakili item pada setiap transaksi yang ada.
 * 
 * Sebuah transaksi seperti pembelian atau penjualan dapat terdiri atas
 * lebih dari satu Item.
 * 
 * Harga barang yang diperdagangkan tidak harus sama dengan yang tertera
 * di barang (misalnya harga berdasarkan kesepakatan kerja sama 
 * dengan pihak terkait).
 * 
 * Setiap Item boleh memiliki diskon masing-masing
 * (boleh lebih dari satu nilai persentase diskon untuk sebuah Item).
 * 
 * @author SolidSnake
 *
 */
class Item {

   /**
    * Merujuk pada `Barang` yang diperdagangkan.
    * 
    * @var Barang
    */
   public $barang;

   /**
    * Jumlah yang diperdagangkan untuk `Barang` ini.
    *  
    * @var int
    */
   public $jumlah;

   /**
    * Harga yang disepakati untuk transaksi `Barang` ini.
    * 
    * @var double
    */
   public $harga;

   /**
    * Diskon untuk item ini.
    * 
    * @var Diskon 
    */
   public $diskon;

   /**
    *
    * @param Barang $barang
    * @param int $jumlah
    * @param double $harga
    */
   public function __construct($barang, $jumlah, $harga = NULL, $diskon = NULL) {
       $this->barang = $barang;
       $this->jumlah = $jumlah;     
       $this->harga = $harga?: $barang->harga;               
       $this->diskon = $diskon?: new Diskon();
   }

   /**
    * Mengembalikan total harga untuk Item ini.  Perhitungan dilakukan
    * berdasarkan jumlah item * harga, kemudian dikurangi diskon.
    * 
    * @return double
    */
   public function getTotal() {        
       return $this->diskon->setelahDiskon($this->harga * $this->jumlah);
   }

}

/**
 * Class ini mewakili diskon untuk sebuah transaksi atau item transaksi.
 * 
 * @author SolidSnake
 *
 */
class Diskon {

   /**
    * Sebuah array yang berisi nilai diskon untuk item ini.
    * Sebuah item dapat memiliki lebih dari satu diskon, misalnya
    * diskon member dan diskon promosi natal.
    *
    * Diskon dalam bentuk angka, misalnya 25 untuk 25%,
    * 10 untuk 10% dan sebagainya.
    *
    * @var int[]
    */

   public $arrDiskon;

   /**
    * 
    * @param int[] $arrDiskon
    */
   public function __construct($arrDiskon = NULL) {
       if ($arrDiskon == NULL) {
           $this->arrDiskon = [];
       } else if (is_array($arrDiskon)) {
           $this->arrDiskon = $arrDiskon;
       } else {
           $this->arrDiskon = [$arrDiskon];
       }
   }

   /**
    * Menambah nilai diskon baru, misalnya 10 untuk 10% dan 25 untuk 25%.
    * 
    * Bila nilai `diskon` berupa array, maka lebih dari satu nilai akan
    * ditambahkan ke nilai diskon saat ini.
    * 
    * @param mixed $diskon
    */
   public function tambahDiskon($diskon) {
       $this->arrDiskon[] = $diskon;
   }

   /**
    * Menghitung jumlah diskon untuk sebuah harga.  Sebagai contoh,
    * diskon 10% untuk 100.000 akan mengembalikan nilai 10.000.
    * 
    * @param double $harga
    */
   public function jumlahDiskon($harga) {      
       $total = 0;             
       foreach ($this->arrDiskon as $diskon) {
           if ($total==0) {
               $total = $harga * ($diskon/100);
               $harga -= $total;
           } else {
               $diskon = $harga * ($diskon/100);
               $total += $diskon;
               $harga -= $diskon;                          
           }
       }
       return $total;
   }

   /**
    * Menghitung sebuah harga setelah dikurangi diskon ini.
    * Sebagai contoh, diskon 10% untuk 100.000 akan mengembalikan
    * nilai 90.000.
    *  
    * @param double $harga 
    */
   public function setelahDiskon($harga) {
       if (count($this->arrDiskon)==0) return $harga;
       $total = 0;
       foreach ($this->arrDiskon as $diskon) {
           if ($total==0) {
               $total = $harga * (1 - $diskon/100);
           } else {
               $total *= (1 - $diskon/100);
           }
       }
       return $total;
   }

}

/**
 * Class ini dipakai untuk mewakili barang yang diperdagangkan.
 * 
 * @author SolidSnake
 *
 */
class Barang {

   /**
    * Nama barang
    * 
    * @var string
    */
   public $nama;

   /**
    * Harga barang
    * 
    * @var double
    */
   public $harga;

   /**
    * 
    * @param string $nama
    * @param double $harga
    */
   public function __construct($nama, $harga) {
       $this->nama = $nama;
       $this->harga = $harga;
   }
}

?>

Pada kode program di atas, saya mendefinisikan seluruh class pada satu file yang sama yaitu domain.php. Hal ini tidak disarankan karena lebih baik memisahkan masing-masing class pada file PHP tersendiri. Bagi saya secara pribadi, salah satu alasannya adalah karena monitor zaman sekarang cenderung lebih lebar secara horizontal (widescreen) dan lebih sempit secara vertikal. Dengan demikian, membuka beberapa tab secara bersamaan dilengkapi navigasi yang nyaman (Ctrl+PageUp dan Ctrl+PageDown untuk berpindah tab di Zend Studio) akan lebih efektif dibandingkan dengan melakukan scrolling naik turun di file yang sama.

Domain model di atas terdiri atas 4 class yaitu Barang, Diskon, Item dan Penjualan. Mereka memiliki logic untuk menghitung total yang sudah termasuk diskon, dimana masing-masing Item dan Penjualan dapat memiliki satu atau lebih nilai diskon. Sampai disini, saya tentu tidak tahu apakah kode program di atas benar atau salah karena belum ada output yang dapat dilihat! Semakin banyak kode program yang ditulis, saya semakin bimbang melangkah demi masa depan. Agar lebih yakin dan lebih percaya diri, saya segera membuat unit test dengan menggunakan PHPUnit.

Saya mulai dengan men-klik kanan nama proyek, memilih menu New, PHPUnit Test Case seperti yang terlihat pada gambar berikut ini:

Membuat test case baru

Membuat test case baru

Pada dialog yang muncul, saya akan mengisinya seperti berikut ini:

Test case untuk class Diskon

Test case untuk class Diskon

Saya akan menguji mulai dari class yang lebih sederhana. Karena kode program pada class Barang tidak ada yang istimewa, maka saya mulai dengan class Diskon. Setelah men-klik tombol Finish, Zend Studio akan menambahkan dependency ke library PHPUnit. Selain itu, ia akan membuat file DiskonTest.php yang sudah memiliki skeleton yang tinggal saya ubah menjadi seperti berikut ini:

<?php
require_once 'domain.php';
require_once 'PHPUnit/Framework/TestCase.php';

class DiskonTest extends PHPUnit_Framework_TestCase {

  protected function setUp() {
      parent::setUp ();
  }

  protected function tearDown() {
      parent::tearDown ();
  }

  public function testConstruct() {
      $diskon = new Diskon();
      $this->assertNotNull($diskon->arrDiskon);
      $this->assertEmpty($diskon->arrDiskon);

      $diskon = new Diskon(10);
      $this->assertEquals(10, $diskon->arrDiskon[0]);

      $diskon = new Diskon([10, 20]);
      $this->assertEquals([10, 20], $diskon->arrDiskon);        
  }

  public function testTambahDiskon() {
      $diskon = new Diskon();

      $diskon->tambahDiskon(10);       
      $this->assertEquals([10], $diskon->arrDiskon);

      $diskon->tambahDiskon(20);
      $this->assertEquals([10, 20], $diskon->arrDiskon);
  }

  public function testJumlahDiskon() {
      $diskon = new Diskon(10);
      $this->assertEquals(15000, $diskon->jumlahDiskon(150000));

      $diskon = new Diskon([10, 20]);
      $this->assertEquals(42000, $diskon->jumlahDiskon(150000));

      $diskon = new Diskon([20, 10]);
      $this->assertEquals(42000, $diskon->jumlahDiskon(150000));

      $diskon = new Diskon([20, 10, 5]);
      $this->assertEquals(47400, $diskon->jumlahDiskon(150000));

      $diskon = new Diskon();
      $this->assertEquals(0, $diskon->jumlahDiskon(150000));
  }

  public function testSetelahDiskon() {
      $diskon = new Diskon(10);
      $this->assertEquals(135000, $diskon->setelahDiskon(150000));

      $diskon = new Diskon([10, 20]);
      $this->assertEquals(108000, $diskon->setelahDiskon(150000));

      $diskon = new Diskon([20, 10]);
      $this->assertEquals(108000, $diskon->setelahDiskon(150000));

      $diskon = new Diskon([20, 10, 5]);
      $this->assertEquals(102600, $diskon->setelahDiskon(150000));

      $diskon = new Diskon();
      $this->assertEquals(150000, $diskon->setelahDiskon(150000));
  }

}
?>

Walaupun kode program sebuah test case terasa repetitif dan sangat membosankan, saya memiliki kesempatan untuk me-mandang dari sisi pengguna class. Pengguna class bisa saja adalah saya sendiri saat membuat controller nanti atau mungkin saja developer lain. Salah satu resep untuk mempelajari penggunaan class yang ada di proyek orang lain (misalnya proyek open-source) adalah dengan melihat test case yang mereka sediakan.

Unit testing juga penting karena saat memikirkan apa saja yang harus diuji pada sebuah class, saya sering kali menemukan ada yang kurang atau ada yang harus saya modifikasi pada class tersebut. Prinsip Test-driven Development (TDD) bahkan mensyaratkan untuk membuat test case terlebih dahulu sebelum membuat implementasinya. Jadi, bila mengikuti metodologi TDD, saya harus membuat DiskonTest.php terlebih dahulu. Saat ini akan ada banyak pesan kesalahan karena class yang diuji belum ada! Lalu, selangkah demi selangkah, saya membuat implementasi Diskon.php hingga setiap pesan kesalahan yang ada hilang.

Untuk menjalankan test case ini, saya men-klik kanan file DiskonTest.php, lalu memilih menu Run As, PHPUnit Test seperti yang terlihat pada gambar berikut ini:

Menjalankan test case

Menjalankan test case

Pada window PHPUnit, saya dapat melihat hasil eksekusi PHPUnit. Sebagai contoh, bila terdapat perilaku pada class Diskon yang tidak sesuai dengan harapan, saya akan memperoleh hasil seperti pada gambar berikut ini:

Hasil pengujian yang mengandung kegagalan

Hasil pengujian yang mengandung kegagalan

Selain menampilkan kegagalan, window PHPUnit juga menampilkan informasi code coverage. Sebagai contoh, saat ini hanya 52% kode program di file domain.php yang sudah dijangkau oleh pengujian (karena saya belum menguji class lain yang juga didefinisikan di file tersebut). Idealnya, 100% kode program harus diuji. Tetapi terkadang ada bagian kode program yang selalu benar dalam setiap kondisi sehingga pengujian padanya tidak wajib dilakukan.

Saya kemudian memperbaiki kesalahan yang berkaitan dengan method getJumlahDiskon() dan setelahDiskon() (bila terdapat kesalahan seperti pada gambar di atas). Saya dapat men-klik tombol Run last test untuk mengulangi pengujian. Bila masih ada kesalahan, saya kembali memperbaiki kode program untuk class Diskon. Saya terus mengulangi siklus ini hingga akhirnya saya memperoleh hasil sukses seperti yang terlihat pada gambar berikut ini:

Hasil pengujian yang sukses semua

Hasil pengujian yang sukses semua

Berikutnya, saya akan membuat test case untuk class Item yang isinya adalah seperti berikut ini:

<?php
require_once 'domain.php';
require_once 'PHPUnit/Framework/TestCase.php';

class ItemTest extends PHPUnit_Framework_TestCase {

  protected function setUp() {
    parent::setUp ();
  }

  protected function tearDown() {
    parent::tearDown ();
  }

  public function testConstruct() {
    $barang = new Barang("ITEM-01", 100000);

    $item = new Item($barang, 10, 120000, new Diskon(10));      
    $this->assertEquals($barang, $item->barang);
    $this->assertEquals(10, $item->jumlah);
    $this->assertEquals(120000, $item->harga);
    $this->assertEquals(10, $item->diskon->arrDiskon[0]);

    $item = new Item($barang, 10);
    $this->assertEquals($barang, $item->barang);
    $this->assertEquals(10, $item->jumlah);
    $this->assertEquals($barang->harga, $item->harga);
    $this->assertEmpty($item->diskon->arrDiskon);
  }

  public function testGetTotal() {
    $barang = new Barang("ITEM-01", 100000);        

    // Harga sama, tanpa diskon
    $item = new Item($barang, 5);
    $this->assertEquals(500000, $item->getTotal());

    // Harga beda, tanpa diskon
    $item = new Item($barang, 5, 120000);
    $this->assertEquals(600000, $item->getTotal());

    // Harga sama, dengan diskon
    $item = new Item($barang, 5, NULL, new Diskon(10));
    $this->assertEquals(450000, $item->getTotal());

    // Harga beda, dengan diskon
    $item = new Item($barang, 5, 120000, new Diskon(10));
    $this->assertEquals(540000, $item->getTotal());
  }
}
?>

Seperti biasa, saya menjalankan test case di atas dan memastikan semuanya baik-baik saja.

Setelah itu, saya kembali membuat test case untuk class Penjualan seperti berikut ini:

<?php
require_once 'domain.php';
require_once 'PHPUnit/Framework/TestCase.php';

class PenjualanTest extends PHPUnit_Framework_TestCase {

  protected function setUp() {
    parent::setUp ();
  }

  protected function tearDown() {
    parent::tearDown ();
  }

  public function testTambahItem() {
    $barang1 = new Barang("BRG-01", 100000);
    $barang2 = new Barang("BRG-02", 200000);

    $penjualan = new Penjualan("INV-01", "2013-12-10");
    $penjualan->tambahItem(new Item($barang1, 10));
    $penjualan->tambahItem(new Item($barang2, 20));

    $this->assertCount(2, $penjualan->arrItem);
    $this->assertEquals($barang1, $penjualan->arrItem[0]->barang);
    $this->assertEquals(10, $penjualan->arrItem[0]->jumlah);
    $this->assertEquals($barang2, $penjualan->arrItem[1]->barang);
    $this->assertEquals(20, $penjualan->arrItem[1]->jumlah);
  }

  public function testGetTotal() {
    $barang1 = new Barang("BRG-01", 100000);
    $barang2 = new Barang("BRG-02", 200000);

    // tanpa diskon
    $penjualan = new Penjualan("INV-01", "2013-12-10");
    $penjualan->tambahItem(new Item($barang1, 10));
    $penjualan->tambahItem(new Item($barang2, 20));
    $this->assertEquals(5000000, $penjualan->getTotal());

    // dengan diskon
    $penjualan = new Penjualan("INV-02", "2013-12-10", new Diskon(10));     
    $penjualan->tambahItem(new Item($barang1, 10));
    $penjualan->tambahItem(new Item($barang2, 20));
    $this->assertEquals(4500000, $penjualan->getTotal());

    // dengan diskon per item + diskon
    $penjualan = new Penjualan("INV-03", "2013-12-10", new Diskon(10));
    $penjualan->tambahItem(new Item($barang1, 10, 120000, new Diskon(5)));
    $penjualan->tambahItem(new Item($barang2, 20));
    $this->assertEquals(4626000, $penjualan->getTotal());
  }

}

Sekarang saya bisa percaya bahwa kode program di domain class yang saya buat dapat bekerja. Ini tidak berarti tidak ada bug pada kode program tersebut, karena test case yang saya buat tidak mencakup seluruh kemungkinan yang terjadi. Tapi setidaknya, setiap kali saya melakukan perubahan pada class tersebut, saya dapat menjalankan ulang test case untuk memastikan bahwa perubahan yang saya buat tidak menimbulkan dampak buruk pada class lainnya.

Pertanyaannya adalah menjalankan 3 test case satu per satu cukup merepotkan, apakah ada cara lain yang lebih singkat? Saya dapat membuat sebuah test suite! Saya akan mulai dengan men-klik kanan nama proyek, memilih New, PHPUnit Test Suite. Saya mengisi dialog yang muncul sehingga terlihat seperti berikut ini:

Membuat test suite

Membuat test suite

Setelah men-klik Finish, Zend Studio akan menghasilkan sebuah file bernama latihanSuite.php yang didalamnya mendaftarkan test case yang telah saya buat sebelumnya. Saya dapat menjalankan test suite ini dengan men-klik kanan pada nama file tersebut, memilih menu Run As, PHPUnit Test. Saya akan memperoleh hasil seperti pada gambar berikut ini:

Menjalankan test suite

Menjalankan test suite

Dengan test suite, saya dapat menjalankan beberapa test case berbeda dengan hanya satu kali pemanggilan.

Perihal Solid Snake
I'm nothing...

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: