Best Practice Testing using Sinon.js

Sinon.js adalah tools yang sangat bermanfaat apabila kita ingin melakukan unit test untuk Java Scripts. Sinon menyediakan fitur untuk melakukan Spies, Stubs, dan Mock. Apa saja itu? Mari kita bahas lebih lanjut.

Pengantar

Membuat testing code dengan menggunakan banyak sekali functional lain seperti Ajax, networking, database, dan dependency lainnya tentu saja akan sangat sulit. Misalnya saja apabila aplikasi kita melakukan perintah Ajax atau networking lainnya, kita harus memastikan server yang akan memberikan response pada request kita. Selain itu penggunaan database harus dipastikan bahwa database kita sudah siap untuk memberikan data. Hal ini berarti test yang dibuat akan semakin sulit karena kita harus mempersiapkan environment agar test berjalan lancar.

Sinon.js hadir menyelesaikan masalah yang ada dengan kemampuannya yaitu spies, stubs dan mocks.

Contoh fungsi

Agar mempermudah penjelasan, mari kita mulai dengan ilustrasi fungsi sebagai berikut.

function setupNewUser(info, callback) {
var user = {
name: info.name,
nameLowercase: info.name.toLowerCase()
};

try {
Database.save(user, callback);
}
catch(err) {
callback(err);
}
}

Fungsi diatas akan menerima dua parameter yaitu sebuah objek info berisi data yang akan dimasukkan kedalam database dan sebuah callback function. Dalam kasus ini, fungsi save yang dilakukan dalam Database adalah untuk memasukkan data yang ada kedalam database.

Spies, Stubs, dan Mocks

Spies, Stubs, dan Mocks sama saja dengan pemeran pengganti (stunts) dalam film. Apa yang mereka lakukan adalah sebagai pengganti dari fungsi yang membutuhkan dependency yang ada pada kode kita.

Spies

Spies digunakan untuk memdapatkan informasi mengenai function call. Lebih lanjut, spy dapat memberitahukan berapa banyak sebuah fungsi dipanggil, apa saja argumen yang dipanggil, apa return value dan error yang di throw, dll.

Spy adalah pilihan yang baik untuk test yang ingin melakukan verifikasi terhadap suatu pemanggilan fungsi. Dikombinasikan dengan assertions, kita bisa mengecek berapa banyak perbedaan yang tidak sesuai dengan harapan test kita.

Biasa spies digunakna untuk:

  • Mengetahui berapa banyak sebuah fungsi dipanggil
  • Mengetahui argumen apa saja yang dipanggil bersamaan dengan fungsi

Ada beberapa fungsi yang bisa digunakan untuk mengecek berapa kali suatu fungsi dipanggil diantaranya:

  • sinon.assert.callCount akan menghitung berapa kali fungsi dipanggil
  • sinon.assert.calledOnce akan memastikan fungsi dipanggil tepat satu kali
  • sinon.assert.notCalled akan memastikan fungsi tidak dipanggil

Misalnya berikut adalah test code yang kita miliki:

it('should call save once', function() {
var save = sinon.spy(Database, 'save');

setupNewUser({ name: 'test' }, function() { });

save.restore();
sinon.assert.calledOnce(save);
});

Selain itu, untuk mengetahui argumen apa yang dipanggil bersamaan dengan fungsi kita bisa menggunakan sinon.assert.calledWith. Berikut adalah contohnya:

it('should pass object with correct values to save', function() {
var save = sinon.spy(Database, 'save');
var info = { name: 'test' };
var expectedUser = {
name: info.name,
nameLowercase: info.name.toLowerCase()
};

setupNewUser(info, function() { });

save.restore();
sinon.assert.calledWith(save, expectedUser);
});

Masih banyak hal lain yang dapat di-assert oleh spy, pada penjelasan ini saya tidak menjelaskan keseluruhan hal yang dapat dilakukan.

Spy tidak akan mengubah behavior dari fungsi, namun apabila kita ingin mengubahnya maka kita bisa menggunakan stub.

Stubs

Stubs sebenarnya menyerupai spies, hanya saja stubs akan menimpa fungsi. Stub bisa saja mengubah behavior dari fungsi seperti mengembalukan nilai tertentu atau mengembalikan error. Stub biasa digunakan untuk:

  • Apabila ingin menimpa fungsi yang membutuhkan dependency lain
  • Apabila ingin melakukan error trigger

Apabila ingin menimpa fungsi yang membutuhkan dependency lain, seperti Database.save pada contoh, kita bisa menggunakan stub, sehingga dalam kasus ini, kita tidak perlu memastikan Database sudah siap digunakan dengan pengaturan yang ada, tetapi akan dibuatkan fungsi pengganti oleh Sinon. Berikut adalah contohnya:

it('should pass object with correct values to save', function() {
var save = sinon.stub(Database, 'save');
var info = { name: 'test' };
var expectedUser = {
name: info.name,
nameLowercase: info.name.toLowerCase()
};

setupNewUser(info, function() { });

save.restore();
sinon.assert.calledWith(save, expectedUser);
});

Dengan menimpa fungsi Database.save kita tidak perlu lagi bersusah payah memikirkan mengenai kesiapan database dalam menangani test.

Selain itu, apabila ingin melakukan testing terhadap error, kita juga bisa menggunakan stub. Berikut adalah contohnya:

it('should pass the error into the callback if save fails', function() {
var expectedError = new Error('oops');
var save = sinon.stub(Database, 'save');
save.throws(expectedError);
var callback = sinon.spy();

setupNewUser({ name: 'foo' }, callback);

save.restore();
sinon.assert.calledWith(callback, expectedError);
});

Mocks

Setelah mengetahui penjelasan mengenai spies dan stubs, sekarang kita akan berkenalan dengan mocks. Mocks pada dasarnya adalah yang paling sakti diantara spies dan stubs, karena mocks dapat melakukan apa yang dapat dilakukan oleh spies dan stubs. Tapi tentu saja terdapat trade off yang siap dibayar apabila menggunakan mocks, yaitu apabila salah dalam penulisan akan mudah sekali terjadi error pada test yang ada karena kita mendefinisikan ekspektasi di awal. Maksudnya dalam mocks kita bisa saja mendefinisikan multiple assertion yang mana harus dicek secara pasti agar tidak terjadi kesalahan. Langsung saja berikut adalah contoh penggunaan mocks:

it('should pass object with correct values to save only once', function() {
var info = { name: 'test' };
var expectedUser = {
name: info.name,
nameLowercase: info.name.toLowerCase()
};
var database = sinon.mock(Database);
database.expects('save').once().withArgs(expectedUser);

setupNewUser(info, function() { });

database.verify();
database.restore();
});

Dalam kasus ini kita memastikan agar fungsi dipanggil sekali dengan fungsi once() dan argument yang digunakan harus sesuai dengan fungsi withArgs(). Jadi dengan mocks, kita dapat menggunakan multiple assertion yang sesuai dengan fungsi yang kita mau test.

Tips

Apabila kita menggunakan fungsi yang sama-sama akan distub dan digunakan dalam beberapa test berbeda, kita dapat melakukan shared stubs dengan fungsi beforeEach(). Sebagai contoh:

describe('Something', function() {
var save;
beforeEach(function() {
save = sinon.stub(Database, 'save');
});

afterEach(function() {
save.restore();
});

it('should do something', function() {
//you can use the stub in tests by accessing the variable
save.yields('something');
});
});

Yang harus dipastikan adalah setelah pemanggilan afterEach() harus melakukan clean up fungsi stub dengan perintah restore().

Apa yang dilakukan minggu lalu

Terdapat beberapa hal yang saya lakukan minggu lalu yaitu:

  • Meningkatkan kualitas testing code agar meningkatkan code coverage (sudah diatas 80%)
  • Melakukan refactoring terhadap fungsi yang ada pada job handlers agar tidak terjadi nested callback.
  • Melakukan integrasi antara backend, database, dan frontend yang ada.
  • Melakukan code review dengan teman-teman.
  • Memastikan branch bersih.