Thursday, June 10, 2010

Unit Testing di PHP

Unit testing itu merupakan test paling dasar dalam pengembangan aplikasi. Terlepas dari apakah aplikasi elo itu ditulis menggunakan paradigma OO atau tidak. Dengan unit testing. lo melakukan sejenis "pembuktian" bahwa fungsi (unit) elo itu melakukan apa yang sebenarnya harus dilakukan. apabila sebuah fungsi (unit) bisa memiliki beberapa kemungkinan. maka sebisa mungkin semua kemungkinan ini di test/buktikan kebenarannya.

misal:
pada fungsi

f(x) = x^2

kita berasumsi bahwa apabila fungsi tersebut diberikan nilai minus pada parameter x, maka hasilnya pasti nilai kuadratnya dalam positif. kalau kita kasih nilai 0 maka hasilnya 0, dan kalo dikasi nilai positif maka hasilnya kuadratnya dalam positif.

maka dari itu, dalam men-test fungsi (unit) ini, kita butuh katakanlah 3 "test case" (kasus kali ya istilahnya), misalnya

jika x = 2 maka f(x) = 4
jika x= 0 maka f(x) = 0
jika x= -2 maka f(x) = 4

apabila waktu lo test, ternyata keluarnya gak sama, berarti ni fungsi "buggy".. harus dibenerin...

so karena ini milis php, dan bukan milis matematika, gue tulis contoh codenya. gue tulis contohnya menggunakan unit testing framework bawaan phpunit. yang penting disini konsepnya dulu, bukan gimana cara pake phpunit.

kelas yang mo di test :
namespace project\iseng;

class MyClass{
  
  public static function myMethod($x){
    return $x * $x;
  }
  
}

contoh unit test nya :
namespace project\tests;

use project\iseng\MyClass;

class MyClassTest extends PHPUnit_Framework_TestCase
{
  
  /**
   * @test
   */
  public function myMethodTestXEquals2()
  {
    $result = MyClass::myMethod(2);
    $this->assertEquals(4, $result);
  }
  
  /**
   * @test
   */
  public function myMethodTestXEquals0()
  {
    $result = MyClass::myMethod(2);
    $this->assertEquals(4, $result);
  }
  
  /**
   * @test
   */
  public function myMethodTestXEqualsMinus2()
  {
    $result = MyClass::myMethod(-2);
    $this->assertEquals(4, $result);
  }
  
}

apabila test ini di jalankan maka phpunit akan lapor OK. semua gak ada masalah. Akan tetapi dalam prakteknya menulis aplikasi. gak jarang kita menghadapi kesalahan dalam implementasi. Kesalahan itu bisa didasari berbagai macam alasan.... misalnya karena tidak paham benar konsep dasar proses bisnisnya. dan lain2.

sekarang gue ambil contoh lah. Anggap programmernya gak ngerti bahwa fungsi f(x) adalah x^2 ... dia cuma tahu hasil dari f(x) adalah 0 untuk 0, 4 untuk 2 dan minus 2. mungkin dia ga tanya lagi ke product ownernya.. atau tidur, waktu pelajaran kalkulus, diferential dan polynomial pas kuliah :D

jadi dia tulis begini :
class MyClass{
  
  public static function myMethod(x){
    return x * 2;
  }
  
}

waktu unit test nya dijalanin.. phpunit bilang OK.. no problem.. tapi waktu di deploy di produktiv server.. hitungannya salah semua.. karena waktu masukin angka 1.. sebenernya hasilnya harus satu, ini malah 2... waktu masukin 3 mestinya 9, ini malah 6... akhirnya softwarenya "buggy".

Sekarang mungkin muncul pertanyaan, gue uda nulis unit test, kok masih buggy? well, yang ngetest kan kita, yang semestinya paham bagaimana bisnis prosesnya harus diimplementasikan kan kita. maka dari itu, kita yang sebenarnya harus tahu lebih dahulu.. fungsi ini sebenernya maunya apa.. dan menulis test case dari segala macam bentuk kemungkinan yang ada. unit test sendiri hanyalah "perangkat" yang ngebantu kita untuk menulis aplikasi yang lebih berkualitas dan stabil.

Dengan sadarnya kita bahwa software kita buggy, dan unit test kita belum meng-cover semua kemungkinan yang ada. solusi yang harus dijalanin adalah tidak hanya mereparasi codenya, tapi juga menambahkan test-case pada unit kita. contoh :
namespace project\tests;

use project\iseng\MyClass;

class MyClassTest extends PHPUnit_Framework_TestCase
{
  
  //test case sebelumnya....
  
  /**
   * @test
   */
  public function myMethodTestXEquals1()
  {
    $result = MyClass::myMethod(1);
    $this->assertEquals(1, $result);
  }
  
  /**
   * @test
   */
  public function myMethodTestXEquals3()
  {
    $result = MyClass::myMethod(3);
    $this->assertEquals(9, $result);
  }
  
}

Unit Testing juga digunakan sebagai "perangkat" dalam bekerja menggunakan metodologi Test Driven Development dimana esensinya adalah "test first, then write the code". Dengan ini kita didorong untuk memahami dulu bisnis prosesnya atau tugas yang harus kita kerjakan. dan mempertimbangkan semua kemungkinan kasus2 yang ada yang akan terjadi ketika fungsi tersebut digunakan. Tulis test case nya dulu. baru menulis codenya.

Dalam hubungannya dengan bahasa berparadigma OO. Tantangan dalam menulis unit test salah satunya adalah interaksi fungsi dengan object. Maksudnya disini. kalo kita nulis satu fungsi/method, biasanya gak selalu parameternya itu angka atau primitiv data type, seperti contoh gue diatas. bisa jadi parameter x itu adalah object Pegawai.. dengan segala member attribute dan methodenya.

Dalam hal ini, ketika menulis unit test. kita harus pastikan bahwa unit test tersebut berdiri secara independen, dan tidak dipengaruhi efek2 samping dari luar.

misalnya f(x,y) adalah menghitung kenaikan gaji pegawai dimana x adalah object pegawai dan y adalah kenaikan dalam persen.
namespace project\iseng;

use project\models;

class MyClass{
  
  public static function hitungKenaikanGajiDalamPersen(models\Pegawai
                                                       $pegawai, $persen){
    $kenaikan = $pegawai->getGaji() * ($persen/100);
    $gajiBaru = $pegawai->getGaji() + $kenaikan;
    $pegawai->setGaji($gajiBaru);
  }
  
}

Apabila kita menulis unit test untuk fungsi ini, dan Object pegawai di initialisasi menggunakan informasi dari database. Maka unit test kita gak akan konstan kebenarannya. apabila di test berulang-ulang. misal :
class MyClassTest extends PHPUnit_Framework_TestCase
{
  
  //test case sebelumnya....
  
  /**
   * @test
   */
  public function myMethodTestNaikGaji10Persen()
  {
    $pegawai = projects\dao\Pegawai::getById(5);
    MyClass::hitungKenaikanGajiDalamPersen($pegawai, 10);
    $this->assertEquals(3300, $pegawai->getGaji());
  }
  
}

Dalam contoh ini, unit test menginitialisasi object pegawai dari database. menganggap si pegawai dg nomer 5 itu gajinya 3000 eur. setelah naik 10 persen jadi 3300. Mungkin pada test pertama benar. karena memang begitu keadaannya dalam database. tapi setelah test pertama. informasi dalam database sudah berubah. yaitu naik gajinya 3300. waktu menjalankan test untuk kedua kalinya maka yang dinaikkan 10% sudah bukan 3000 lagi, melainkan 3300. dan perbandingan yang ada dalam unit test sudah tidak benar lagi. phpunit bakal protes kalo test nya gagal.

Dalam hal ini, unit test mengenal istilah "mock objects".. dimana dalam test. object2 yang berinteraksi dibuatkan object "boong2an" untuk menunjang test tersebut.

contoh standard misalnya :
class MyClassTest extends PHPUnit_Framework_TestCase
{
  
  //test case sebelumnya....
  
  /**
   * @test
   */
  public function myMethodTestNaikGaji10Persen()
  {
    $pegawai = new projects\models\Pegawai();
    $pegawai->setGaji(3000);
    MyClass::hitungKenaikanGajiDalamPersen($pegawai, 10);
    $this->assertEquals(3300, $pegawai->getGaji());
  }
  
}

Dalam hal ini pegawai mungkin attributnya ga cuma gaji saja, tapi ada alamat dan lain sebagainya.. hanya saja karena dalam test ini hanya berurusan masalah gaji. kita membuat instance kosong dari kelas pegawai dan mengisi nilai gajinya sebelum diberikan ke method untuk menghitung kenaikan gaji.

Untuk hal2 yang kompleks umumnya unit testing framework memberikan tools2 extra dimana kita bisa membuat mock objects untuk test yang cukup rumit dan berinteraksi dengan berbagai macam object.

So sekian dulu.. sebagai penutup.. seperti teman2 yang lain bilang.. menulis unit test itu butuh latihan dan disiplin tinggi. gue sendiri saja masih sering gagal menjalankannya secara konsekuen. semua tahu bahwa hal ini menunjang stabilitas dan kualitas produkt. tapi dalam pelaksanaannya gak jarang penuh hambatan...

Selamat belajar.

No comments: