Unit Testing Di JavaScript Dengan QUnit


Pada artikel ini, saya akan mencoba menggunakan QUnit untuk melakukan unit testing pada kode program JavaScript. Berbeda dengan Java dimana unit adalah sebuah class, pada JavaScript, unit adalah sebuah function. Salah satu tool yang dapat dipakai untuk melakukan unit testing pada JavaScript adalah QUnit. Tool ini juga dipakai untuk melakukan pengujian pada proyek jQuery dan jQuery UI. Untuk memakai QUnit, saya dapat men-download-nya di http://qunitjs.com.

Sebagai latihan, saya akan membuat sebuah library sederhana yang mengimplementasikan fungsi require() seperti pada spesifikasi module oleh CommonJS. JavaScript (sebelum ES6) tidak memiliki konsep module. Oleh sebab itu, developer JavaScript seringkali mengalami masalah berkaitan dengan penggunaan banyak library berbeda dan kesalahan penamaan yang sama. Pada Java, hal ini diatasi melalui konsep package dan keyword import. ECMAScript 6 nanti juga akan memperkenalkan konsep module pada JavaScript. Saat ini, untuk sementara ada beberapa spesifikasi yang dapat dipakai dalam mengatur kode program JavaScript agar lebih standar dalam mengelola module. Salah satunya adalah yang dibuat oleh CommonJS (http://www.commonjs.org/specs/modules/1.0/). Yang ditawarkan adalah koleksi design pattern yang bisa diterapkan oleh setiap developer JavaScript. Karena ini hanya latihan, saya tidak akan membuat sesuatu yang mengikuti seluruh yang ada di spesifikasi (selain itu, implementasi CommonJS sudah banyak, salah satunya adalah Node.js).

Kali ini saya akan membuat unit test dengan mengikuti filosofi test driven development (TDD). Dengan TDD, saya mulai dengan membuat test case terlebih dahulu baru kemudian membuat implementasi kode programnya. Yup! Membuat unit test dulu baru membuat kode program! Saya pernah mendengar cerit bahwa seorang dosen S2 yang sedang mendidik dosen S1 guna sertifikasi mengatakan bahwa harus ada GUI (tampilan atau output) terlebih dahulu baru bisa menguji kode program. Tentu saja ia salah. Lalu, bagaimana cara mengujinya?

Saya akan mulai dengan membuat test case yang mengikuti spesifikasi CommonJS dengan membuat 2 file JavaScript yang mewakili module, yaitu moduleA.js dan moduleB.js. Isi dari moduleA.js adalah:

exports.foo = require("moduleB").foo;

Sementara itu, isi dari moduleB.js adalah:

exports.foo = function() {};

Pada spesifikasi CommonJS, fungsi require() dipakai untuk menyertakan modul lain. Sebuah modul yang ingin men-export function atau nilai agar dapat dipakai oleh modul lainnya harus mengisinya pada exports yang selalu tersedia. Pada contoh di atas, moduleA akan memakai moduleB dimana function foo() dari moduleA persis sama seperti function foo() dari moduleB.

Berikutnya, saya akan membuat sebuah halaman HTML sederhana yang mewakili sebuah test case dengan nama test.html yang isinya seperti berikut ini:

<!doctype html>
<html>
<head>
    <title>Latihan QUnit</title>
    <link rel="stylesheet" href="qunit-1.14.0.css" />
    <script src="qunit-1.14.0.js"></script>
    <script src="myrequire.js"></script>
    <script>
        test("Menguji module", function() {
            var moduleA = require("moduleA");
            var moduleB = require("moduleB");
            ok(moduleA!=undefined, "moduleA harus punya nilai");
            ok(moduleB!=undefined, "moduleB harus punya nilai");
        });
        test("Menguji hasil export di module", function() {
            var moduleA = require("moduleA");
            var moduleB = require("moduleB");
            ok(moduleA.foo!=undefined, "moduleA.foo harus ada");          
            ok(moduleB.foo!=undefined, "moduleB.foo harus ada");
        });
        test("Menguji function yang di-export", function() {
            var moduleA = require("moduleA");
            var moduleB = require("moduleB");     
            equal(moduleA.foo, moduleB.foo);
        });
    </script>
</head>
<body>
    <div id="qunit"></div>
</body>
</html>

Bila saya menjalankan HTML di atas, saya akan menemui tampilan seperti pada gambar berikut ini:

Hasil awal pengujian menunjukkan semua test gagal

Hasil awal pengujian menunjukkan semua test gagal

Jangan kaget! Di TDD, pada awalnya seluruh test akan gagal. Tujuan dari TDD adalah bagaimana membuat test yang gagal tersebut menjadi benar dan sukses (ini adalah arti kata test-driven dimana pemicu pembuatan kode program adalah test case). Progress atau perkembangan proyek juga bisa dilihat berdasarkan jumlah test yang sukses atau gagal.

Sebagai langkah berikutnya, saya akan membuat sebuah file JavaScript baru bernama myrequire.js yang mewakili kode program yang hendak diuji dengan isi seperti berikut ini:

(function(global) {
    function require(moduleId) {            
        var module = {};
        return module;
    }       

    global.require = require;
}(this));

Saya kemudian menambahkan baris berikut ini pada test.html:

<script src="myrequire.js"></script>

Sekarang, bila saya menampilkan file tersebut pada browser, akan ada 1 test case yang sukses, namun yang lainnya masih gagal, seperti yang terlihat pada gambar berikut ini:

Hasil pengujian menunjukkan test case pertama sudah sukses

Hasil pengujian menunjukkan test case pertama sudah sukses

Saya kembali melakukan perubahan di myrequire.js agar test case berikutnya menjadi sukses. Sebagai contoh, saya mengubahnya menjadi seperti berikut ini:

(function(global) {
    function require(moduleId) {            
        var module = {};
        module.foo = function() {};
        return module;
    }       

    global.require = require;
}(this));

Sekarang, bila saya menjalankan pengujian QUnit, saya akan memperoleh hasil seperti pada gambar berikut ini:

Hasil pengujian menunjukkan test case pertama dan kedua sudah sukses

Hasil pengujian menunjukkan test case pertama dan kedua sudah sukses

Sudah semakin baik bukan? Hanya saja, agar semua test sukses, kode program saya perlu membaca file JavaScript yang mewakili module. Untuk itu, saya perlu memperkenalkan sebuah function baru yang membaca file JavaScript dengan menggunakan XHR. Seperti biasa, pada TDD, sebelum membuat kode program, saya terlebih dahulu membuat test case-nya. Untuk itu, saya membuat file testBacaFile.html yang isinya seperti berikut ini:

<!doctype html>
<html>
<head>
    <title>Latihan QUnit</title>
    <link rel="stylesheet" href="qunit-1.14.0.css" />
    <script src="qunit-1.14.0.js"></script>
    <script src="myrequire.js"></script>
    <script>
        test("Membaca module dalam direktori yang sama", function() {         
            equal('exports.foo = require("moduleB").foo;', bacaFile("moduleA.js"));
            equal('exports.foo = function() {};', bacaFile("moduleB.js"));
        });             
        test("Membaca module yang tidak ada", function() {
            throws(function() {
                bacaFile("entahDimana.js");
            });
        });
    </script>
</head>
<body>
    <div id="qunit"></div>
</body>
</html>

Pada pengujian di atas, saya memastikan bahwa bacaFile() akan mengembalikan string yang sesuai dengan isi file JavaScript. Bila saya berusaha membaca file yang tidak ada, function tersebut harus mengembalikan sebuah exception. Pada QUnit, saya menggunakan throws() untuk memeriksa apakah ada exception yang dikembalikan (dan saya juga bisa memeriksa jenis exception yang dikembalikan bila perlu).

Seperti biasa, bila saya menjalankan pengujian di atas, saya akan menemukan kegagalan karena function bacaFile() belum ada. Oleh sebab itu, saya perlu membuatnya dengan mengubah kode program myrequire.js menjadi seperti berikut ini:

(function(global) {

    function require(moduleId) {            
        var module = {};
        module.foo = function() {};
        return module;
    }       

    function bacaFile(namaFile) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", namaFile, false); 
        return xhr.responseText;        
    };

    global.require = require;
}(this));

Saya kemudian kembali menjalankan unit test di testBacaFile.html, tapi saya kembali mendapatkan pesan kesalahan yang sama bahwa bacaFile is not defined. Mengapa demikian? Hal ini karena function bacaFile() berada dalam closure sehingga tidak dapat diakses dari luar (termasuk oleh test case). Saya tidak bisa menguji sesuatu yang tidak bisa saya akses. Untuk itu, saya akan mengganti penggunakan closure menjadi sesuatu yang lebih OOP dengan mengubah kode program di myrequire.js menjadi seperti berikut ini:

function MyRequire(exports) {
    this.exports = exports;
};

MyRequire.prototype = {

    require: function require(moduleId) {           
        var module = {};
        module.foo = function() {};
        return module;
    },

    bacaFile: function bacaFile(namaFile) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", namaFile, false); 
        return xhr.responseText;        
    },

}

this.myRequire = new MyRequire(typeof exports === 'object' && exports || this);
this.require = function(s) { return this.myRequire.require(s); };

Walaupun cara diatas menyebabkan ada variabel global myRequire, setidaknya ini akan mempermudah pengujian yang merupakan kriteria penting dalam TDD. Sekarang, saya bisa mengubah kode program di testBacaFile.html menjadi seperti berikut ini:

<!doctype html>
<html>
<head>
    <title>Latihan QUnit</title>
    <link rel="stylesheet" href="qunit-1.14.0.css" />
    <script src="qunit-1.14.0.js"></script>
    <script src="myrequire.js"></script>
    <script>
        test("Membaca module dalam direktori yang sama", function() {         
            equal('exports.foo = require("moduleB").foo;', myRequire.bacaFile("moduleA.js"));
            equal('exports.foo = function() {};', myRequire.bacaFile("moduleB.js"));
        }); 
        test("Membaca module yang tidak ada", function() {
            throws(function() {
                myRequire.bacaFile("entahDimana.js")
            });
        });         
    </script>
</head>
<body>
    <div id="qunit"></div>
</body>
</html>

Tapi bila saya menampilkan HTML tersebut, kode program saya masih salah. File masih belum dibaca dengan benar. Mengapa demikian? Frustasi adalah hal biasa bagi developer. Setidaknya TDD membantu saya mengetahui bahwa ada yang salah dengan kode program sebelum library ini di-integrasi-kan pada proyek web yang lebih kompleks lagi.

Saya akhir menemukan bahwa saya lupa memanggil xhr.send()! Pantas saja gagal, saya segera menambahkan xhr.send() setelah xhr.open() di function bacaFile(). Sekarang, bila saya menampilkan testBacaFile.html, maka test case pertama akan sukses seperti pada gambar berikut ini:

Hasil pengujian menunjukkan test pertama sudah sukses

Hasil pengujian menunjukkan test pertama sudah sukses

Berikutnya, bagaimana caranya agar test case kedua tidak gagal? Saya harus men-throw sesuatu bila terjadi kegagalan saat membaca file. Sebagai contoh, saya mengubah kode program bacaFile() di myrequire.js menjadi seperti berikut ini:

...
bacaFile: function bacaFile(namaFile) {
    var xhr = new XMLHttpRequest();     
    xhr.open("GET", namaFile, false);
    xhr.send();
    if (xhr.status===200) {
        return xhr.responseText;
    } else {
        throw "Can't read file: " + namaFile;
    }       
},
...

Sekarang, seluruh pengujian untuk method bacaFile() akan sukses seperti yang terlihat pada gambar berikut ini:

Hasil pengujian menunjukkan seluruh test case sudah sukses

Hasil pengujian menunjukkan seluruh test case sudah sukses

Dampak dari unit test adalah membuat saya lebih percaya diri. Saya setidaknya bisa yakin bahwa bacaFile() sudah bekerja sesuai dengan yang diharapkan.

Perkembangannya terasa dengan jelas, bukan? Sampai disini saya bisa istirahat sejenak, menikmati segelas teh hangat atau jalan-jalan sebentar. Setelah saya kembali, saya tahu apa yang harus dilakukan. Masih ada test case yang gagal🙂

Agar semua test case di test.html sukses, saya mengubah kode program pada myrequire.js menjadi seperti berikut ini:

function MyRequire(exports) {
    this.exports = exports;
    this.modules = new Map();
};

MyRequire.prototype = {

    require: function require(moduleId) {           
        var module = {id: moduleId, exports: {}};           

        // TIdak melakukan loading bila sudah pernah di-load    
        if (this.modules.has(moduleId)) return this.modules.get(moduleId).exports;

        // Membaca file JS yang mewakili module
        new Function("module", "exports", this.bacaFile(moduleId + ".js"))
            .call(this.exports, module, module.exports);
        this.modules.set(moduleId, module);                     

        return module.exports;
    },

    bacaFile: function bacaFile(namaFile) {
        var xhr = new XMLHttpRequest();     
        xhr.open("GET", namaFile, false);
        xhr.send();
        if (xhr.status===200) {
            return xhr.responseText;
        } else {
            throw "Can't read file: " + namaFile;
        }       
    },

}

this.myRequire = new MyRequire(typeof exports === 'object' && exports || this);
this.require = function(s) { return this.myRequire.require(s); };

Sekarang, bila saya menjalankan kode program, seluruh test yang ada akan sukses seperti pada yang terlihat pada gambar berikut ini:

Hasil pengujian menunjukkan seluruh test case sudah sukses

Hasil pengujian menunjukkan seluruh test case sudah sukses

Sebagai informasi, QUnit juga mendukung pola module di CommonJS sehingga saya dapat memanggilnya melalui require(). Sebagai contoh, saya akan membuat unit test lagi yang diambil dari halaman dokumentasi CommonJS di http://www.commonjs.org/specs/modules/1.0/. Saya membuat file math.js yang isinya seperti berikut ini (sesuai dengan example di website tersebut):

exports.add = function() {
    var sum = 0, i = 0, args = arguments, l = args.length;
    while (i < l) {
        sum += args[i++];
    }
    return sum;
}

Kemudian, saya membuat file increment.js yang isinya seperti berikut ini (sesuai dengan example di website CommonJS):

var add = require('math').add;
exports.increment = function(val) {
    return add(val, 1);
};

Setelah itu, saya akan membuat unit test di file testSample.html yang isinya seperti berikut ini:

<!doctype html>
<html>
<head>
    <title>Latihan QUnit</title>
    <link rel="stylesheet" href="qunit-1.14.0.css" /> 
    <script src="myrequire.js"></script>
    <script>
        var QUnit = require('qunit-1.14.0');

        test('Sample Common JS', function() {
            var inc = require('increment').increment;
            var a = 1;
            equal(2, inc(a));
            var b = 5;
            equal(6, inc(b));
        });             
    </script>
</head>
<body>
    <div id="qunit"></div>
</body>
</html>

Bila saya menampilkan halaman di atas pada browser, saya akan memperoleh tampilan yang menunjukkan bahwa kode program saya bekerja sesuai harapan seperti yang terlihat pada gambar berikut ini:

Hasil pengujian menunjukkan seluruh test case sudah sukses

Hasil pengujian menunjukkan seluruh test case sudah sukses

Cara di atas, dimana saya membuat unit test setelah selesai membuat kode program, adalah sesuatu yang melanggar prinsip test driven development (TDD). Pada TDD, saya harus membuat atau mengembangkan unit test terlebih dahulu sebelum membuat kode program. Tapi tentu saja menambah unit test belakangan bukan sesuatu yang salah. Semakin banyak test case yang ada akan membuat sebuah proyek menjadi semakin teruji dan handal.

Perihal Solid Snake
I'm nothing...

3 Responses to Unit Testing Di JavaScript Dengan QUnit

  1. tonbad mengatakan:

    mantap mas uji cobanya… btw klo buat quiz ‘javascript’ dengan qunit bisa g mas? atau klo bisa gimana ya caranya?… thanks sebelumnya…

    • Solid Snake mengatakan:

      QUnit ditujukan untuk keperluan unit testing sehingga tidak menambah fitur aplikasi.

      Unit testing dipakai untuk menguji kebenaran kode program yang kita buat.

      Sebagai contoh, pada sebuah aplikasi inventory, proses pembelian harus menyebabkan stok produk bertambah, proses penjualan menyebabkan stok produk berkurang, proses retur jual menyebabkan stok produk bertambah, proses retur beli menyebabkan stok produk berkurang, proses penyesuaian bisa menambah atau mengurangi stok produk. Hal ini belum ditambah lagi masing-masing proses bisa di-hapus atau di-update oleh pengguna (yg harus menyebabkan perubahan stok lagi).

      Bagaimana cara memastikan kode program benar? Melakukan pengujian manual harus melibatkan pemeriksaan yang sangat seksama pada seluruh proses di atas dan tingkat akurasinya tidak begitu tinggi (misalnya semakin buru-buru biasanya semakin ga teliti).

      Contoh lain kelemahan pemeriksaan manual adalah bila perubahan kode program bisa secara tak sengaja berdampak buruk pada modul lain. Hal ini sering kali terlewatkan bila tidak ada pengujian secara otomatis yang menyeluruh.

      Unit testing menjawab kebutuhan tersebut dengan melakukan pengujian otomatis. Pada sebuah unit test, developer mengerjakan sebuah proses, misalnya melakukan pembelian, lalu memeriksa apakah nilai stok produk bertambah atau tidak. Bila tidak bertambah, maka test ini gagal dan developer akan mendapatkan notifkasi test yang gagal. Idealnya, seluruh kode program harus dicakup oleh unit testing.

      Bila pengujian melibatkan database, ini disebut integration testing. Sebagai contoh, pada proyek inventory, saya memiliki integration testing yang berjalan sekitar 30 menit. Saya selalu menjalankan integration testing sebelum men-deploy aplikasi saya untuk memastikan tidak ada kesalahan fatal. Walaupun sudah diuji secara otomatis, tidak berarti aplikasi bebas bug. Kesalahan tak terduga akan selalu ada, tapi tingkatnya tidak akan setinggi bila dibandingkan pengujian yang masih dilakukan secara manual (kecuali pengujian manual dilakukan oleh tim khusus yang berpengalaman di bidang quality assurance).

      Pada proyek open-source, unit test menjadi sebuah kewajiban! Masing-masing developer pada saat menambah sebuah fasilitas baru wajib menyertakan unit test untuk fasilitas tersebut. Dengan demikian, developer lain yang melakukan perubahan di masa depan dapat mengetahui bila perubahan yang mereka buat membuat fasilitas lama menjadi tidak bekerja.

      Membuat quiz JavaScript adalah hal lain dan bukan tujuan utama dari terciptanya QUnit.

  2. Adoel Rachman mengatakan:

    Alhamdulillah, mencerahkan gan, ane udah belajar TDD-nya Jasmine, & BDD-nya namun masih belum nemu maksud, dan tujuan mendalam tentang Teting kode Javascript.

    Sekarang, lebih tercerahkan lagi with artikel ini, thanks yaa gan

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: