Skip to main content

Unit Test

NodeJS Unit Test - Jest

Slide PPT : https://docs.google.com/presentation/d/1-WdE65sFR6fc-klYXtuWvM8MMOJQv3Lj

  • NodeJS sendiri sebenarnya memiliki package untuk melakukan assertion, namun kita tidak akan membahasnya, karena jarang sekali orang menggunakan package tersebut
  • Programmer NodeJS kebanyakan menggunakan library yang lebih baik untuk melakukan unit test

Sebenarnya ada banyak sekali library opensource yang bisa kita gunakan untuk melakukan unit test di NodeJS, antara lain :

Namun disini saya akan menggunakan Jest.

Pengenalan Jest

  • Jest adalah salah satu library untuk unit test NodeJS yang sangat populer
  • Jest sendiri dibuat oleh Facebook
  • Jest terintegrasi sangat baik dengan banyak teknologi seperti NodeJS, ReactJS, VueJS, dan lain-lain
  • Jest fokus pada kesederhanaan, sehingga penggunaannya sangat mudah untuk pemula yang ingin mencoba unit test

Menginstall Jest

Jest digunakan untuk membuat unit test saja, sehingga kita tidak perlu menambahkan sebagai dependency production, kita cukup tambahkan sebagai development dependency.

Kita bisa tambahkan di package.json atau gunakan perintah :

npm install jest --save-dev

Next, buat script test di package.json :

{
// ...
"script": {
"test": "jest"
}
// ...
}

Selengkapnya : https://www.npmjs.com/package/jest

Kekurangan Jest

  • Sejak awal belajar NodeJS, kita selalu menggunakan JavaScript Modules
  • Sayangnya, Jest sampai dibuatnya materi ini, belum mendukung JavaScript Modules, masih menggunakan cara lama menggunakan CommonJS dengan memanfaatkan function require()
  • Untungnya, ada library bernama Babel, yang bisa kita gunakan untuk membantu Jest

Babel

  • Babel adalah JavaScript Compiler, yang digunakan untuk melakukan kompilasi kode JavaScript ke kode JavaScript yang berbeda versi, biasanya untuk ke versi yang lebih lama agar kompatibel dengan Browser versi lama.
  • Dengan Babel, kita bisa membuat kode program dengan fitur JavaScript terbaru, seperti Modules, tapi bisa di compile menjadi kode JavaScript lama sehingga compatible ketika dijalankan oleh teknologi lama atau yang belum mendukung fitur JavaScript baru.

Selengkapnya https://babeljs.io/

Integrasi Babel dan Jest

Jest terintegrasi dengan baik dengan Babel, sehingga Jest bisa secara otomatis melakukan kompilasi kode JavaScript unit test kita dengan Babel, dan menjalankan kode JavaScript dengan versi yang kompatibel dengan Jest.

Install :

npm install --save-dev babel-jest
npm install @babel/preset-env --save-dev

tambahkan di file package.json :

{
"scripts": {
"test": "jest"
},
"jest": {
"transform": {
"^.+\\.[t|j]sx?$": "babel-jest"
}
}
}

Next, create file babel.config.json :

babel.config.json
{
"presets": ["@babel/preset-env"]
}

Selengkapnya : https://babeljs.io/setup

Menjalankan Jest

Buat file nama.test.js

test/nama.test.js
// some test code

Jalankan perintah terminal :

npm run test

Atau bisa menggunakan NPX :

npx jest
note
  • Di NodeJS terdapat program bernama NPX (Node Package Runner)
  • NPX ini digunakan spesial untuk menjalankan perintah yang bisa secara otomatis mendeteksi file yang terdapat di node_modules/.bin/

Membuat Unit Test

  • Jest sudah menyediakan function yang diregistrasikan secara global bernama test(), function tersebut digunakan untuk membuat unit test
  • test() memiliki parameter nama unit test dan juga function yang berisi kode unit test nya
name.test.js
test("should ...", () => {
// some code
});

Jest Configuration

  • Jest memiliki banyak konfigurasi, namun jika kita tidak ubah konfigurasinya, Jest sudah memiliki default konfigurasi
  • Ada banyak sekali konfigurasi yang terdapat di Jest, kita akan bahas sambil berjalan, dan yang memang diperlukan saja
  • Jest sendiri mendukung dua cara untuk menyimpan data konfigurasi

Detail Konfigurasi : https://jestjs.io/docs/configuration

  1. Pertama, menyimpan di file package.json dengan key jest
package.json
{
// ...
"jest": {
"verbose": true
}
}
  1. Kedua dengan menyimpan sebagai file JavaScript di file jest.config.js/ts/mjs, atau membuatnya secara otomatis dengan perintah :
jest --init
jest.config.json
{
"bail": 1,
"verbose": true
// some configuration
}
note

Jika menggungkan konfigurasi menggunakan file jest.config.js/ts/mjs, jangan lupa untuk memindahkan konfigurasi Jest di package.json

Konfigurasi di Jest sangat sederhana, cukup gunakan key-value, dimana kita bisa melihat semua konfigurasi key yang tersedia dan kegunaannya di halaman https://jestjs.io/docs/configuration

Matchers

Saat kita membuat unit test, hal yang dilakukan adalah kita biasanya memiliki ekspektasi Contoh pada kode sum() sebelumnya, ketika kita panggil function sum() dengan parameter 1 dan 2, ekspektasi kita adalah hasil return dari function sum() tersebut adalah 3 Di Jest, hal ini dinamakan Matchers

Detail : https://jestjs.io/docs/using-matchers

Expect Function

  • Matchers di Jest direpresentasikan dalam sebuah function bernama expect(value)
  • Function expect() mengembalikan object Matchers, yang bisa kita gunakan untuk mengetest value yang kita expect()
  • Ada banyak sekali function untuk melakukan test di Matchers, kita bisa baca detail nya di halaman dokumentasi API untuk function expect()

Detail : https://jestjs.io/docs/expect

export const sum = (first, second) => {
return first + second;
};

// code testing
test("test sum function", () => {
const result = sum(1, 2);
expect(result).toBe(3);
});

Equals Matchers

  • Salah satu Matchers yang biasa digunakan ketika membuat unit test adalah equals matchers
  • Ini digunakan untuk memastikan bahwa data sesuai atau sama dengan ekspektasi kita
FunctionKeterangan
expect(value).toBe(expected)Value sama dengan expected, biasanya digunakan untuk value bukan object
expect(value).toEqual(expected)Value sama dengan expected, dimana membandingkan semua properties secara recursive, atau dikenal dengan deep equality
// toBe()
expect(hello).toBe("hello joko");

// toEqual()
expect(person).toEqual({ id: 1, name: "joko santoso" });
note
  • toEquel() biasanya untuk value yang bernilai object , array, dll

Truthiness Matchers

  • Dalam unit test, kadang kita ingin membedakan antara undefined, null dan false.
  • Dan kadang kita ingin melakukan ekspektasi nilai tersebut
  • Jest memiliki matchers untuk melakukan hal tersebut juga
FunctionKeterangan
expect(value).toBeNull()Memastikan value adalah null
expect(value).toBeUndefined()Memastikan value adalah undefined
expect(value).toBeDefined()Kebalikan dari toBeUndefined()
expect(value).toBeTruthy()Memastikan value bernilai apapun, asal if statement menganggap true
expect(value).toBeFalsy()Memastikan value bernilai apapun, asal if statement menganggap false
test("truthiness matcher", () => {
let value = null;
expect(value).toBeDefined();
expect(value).toBeNull();
expect(value).toBeFalsy();

value = undefined;
expect(value).toBeUndefined();
expect(value).toBeFalsy();

value = "Joko";
expect(value).toBe("Joko");
expect(value).toBeTruthy();
});

Numbers Matchers

  • Jest juga memiliki matchers untuk digunakan untuk value berupa number
  • Ketika value berupa number, kita juga bisa menggunakan toBe() dan toEqual(), untuk memastikan bahwa number bernilai sama dengan expected
FunctionKeterangan
.toBeGreaterThan(n)Memastikan value lebih besar dari n
.toBeGreaterThanOrEqual(n)Memastikan value lebih besar atau sama dengan n
.toBeLessThan(n)Memastikan value lebih kecil dari n
.toBeLessThanOrEqual(n)Memastikan value lebih kecil atau sama dengan n
test("numbers matcher", () => {
const value = 2 + 2;

expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(4);

expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4);

expect(value).toBe(4);
});

Strings Matchers

  • Jest juga memiliki matchers function yang digunakan untuk value berupa String
  • Jika kita ingin memastikan sebuah string sama, kita bisa gunakan toBe() atau toEqual()
  • Namun kita bisa menggunakan toMatch(regex) untuk memastikan value sesuai dengan regex
test("string matcher", () => {
const name = "Joko Santoso";

expect(name).toBe("Joko Santoso");
expect(name).toMatch(/San/);
});

Arrays Matchers

  • Jest juga memiliki function yang bisa kita gunakan untuk mengecek data di dalam sebuah value array
  • Jika ingin memastikan bahwa array sama, kita bisa menggunakan toEqual()
FunctionKeterangan
.toContain(item)Memastikan value array memiliki item, dimana pengecekan item menggunakan toBe()
.toContainEqual(item)Memastikan value array memiliki item, dimana pengecekan item menggunakan toEqual()
test("array simpel", () => {
const names = ["joko", "roy", "samsul"];

expect(names).toEqual(["joko", "roy", "samsul"]);
expect(names).toContain("joko");
});

test("array object", () => {
const persons = [{ name: "joko" }, { name: "roy" }, { name: "samsul" }];

expect(persons).toEqual([{ name: "joko" }, { name: "roy" }, { name: "samsul" }]);
expect(persons).toContainEqual({ name: "joko" });
});

Exceptions Matchers

  • Saat membuat kode program, kadang kita sering membuat exception
  • Dalam unit test pun, kadang kita berharap sebuah exception terjadi
  • Jest juga memiliki matchers untuk melakukan pengecekan exception
  • Khusus untuk jenis matchers exception, kita perlu menggunakan closure function di value expect() nya, hal ini untuk memastikan exception ditangkap oleh matchers, jika tidak menggunakan closure function, maka exception akan terlanjur terjadi sebelum kita memanggil expect() function
FunctionKeterangan
.toThrow()Memastikan terjadi exception apapu
.toThrow(exception)Memastikan terjadi exception sesuai dengan expected exception
.toThrow(message)Memastikan terjadi exception sesuai dengan string message
export class MyException extends Error {}

export const callMe = (name) => {
if (name === "Joko") {
throw new MyException("Ups my exception heppens!");
} else {
return "Ok";
}
};

test("exception", () => {
//! callMe('Joko');
//! expect(callMe('Joko'));

expect(() => callMe("Joko")).toThrow();
expect(() => callMe("Joko")).toThrow(MyException);
expect(() => callMe("Joko")).toThrow("Ups my exception heppens!");
});

Not Matchers

  • Saat melakukan pengecekan menggunakan matchers, kadang-kadang kita ingin melakukan pengecekan kebalikannya
  • Misal tidak sama dengan, tidak lebih dari, tidak contains, dan lain-lain
  • Jest memiliki fitur untuk melakukan “not” di Matchers nya, dengan menggunakan property not di matchers, secara otomatis kita akan melakukan pengecekan kebalikannya
  • Semua jenis matchers yang sudah kita bahas, mendukung property not ini
test("string.not", () => {
const name = "Joko Santoso";

expect(name).not.toBe("Eko");
expect(name).not.toEqual("Eko");
expect(name).not.toMatch(/Eko/);
});

test("number.not", () => {
const value = 2 + 2;

expect(value).not.toBeGreaterThan(6);
expect(value).not.toBeLessThan(3);
expect(value).not.toBe(10);
});

Test Async Code

  • Saat membuat kode program JavaScript, penggunaan kode asynchronous pasti sering kita gunakan, baik itu menggunakan Promise atau menggunakan Async Await
  • Jest terintegrasi dengan baik jika kita ingin melakukan pengetesan terhadap kode yang async
  • Namun saat kita melakukan pengetesan kode async, kita harus memberi tahu ke Jest, hal ini agar Jest tahu dan bisa menunggu kode async nya, sebelum melanjutkan ke unit test selanjutnya
  • Caranya sebenarnya sangat mudah, kita cukup gunakan async code di closure function Jest
export const sayHelloAsync = (name) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (name) {
resolve(`Hello ${name}`);
} else {
reject("name harus ada.");
}
}, 1000);
});
};

test("async function", async () => {
const result = await sayHelloAsync("Joko");
expect(result).toBe("Hello Joko");
});

test("async mathers", async () => {
await expect(sayHelloAsync("Joko")).resolves.toBe("Hello Joko"); // resolve
await expect(sayHelloAsync()).rejects.toBe("name harus ada."); // reject
});

async Matchers

  • Sebelumnya kita menggunakan async await untuk melakukan matchers, sebenarnya Jest juga memiliki fitur matchers terhadap data async atau Promise
  • Hal ini mempermudah kita ketika ingin melakukan matchers, sehingga tidak perlu melakukan await pada async function nya
  • Semua Async Matchers mengembalikan Promise
FunctionKeterangan
expect(promise).resolvesEkspektasi bahwa promise sukses, dan selanjutnya kita bisa gunakan Matchers function lainnya
expect(promise).rejectsEkspektasi bahwa promise gagal, dan selanjutnya kita bisa gunakan Matchers function lainnya
test("async mathers", async () => {
await expect(sayHelloAsync("Joko")).resolves.toBe("Hello Joko"); // resolve
await expect(sayHelloAsync()).rejects.toBe("name harus ada."); // reject
});

Setup Function

  • Kadang saat membuat unit test, kita membuat kode yang perlu dibuat sebelum unit test berjalan
  • Selain itu, kadang kita juga kita membuat kode yang perlu dilakukan setelah unit test berjalan
  • Jest memiliki fitur untuk menangani kasus seperti ini
FunctionKeterangan
beforeEach(function)Function akan dieksekusi sebelum unit test berjalan, jika terdapat lima unit test dalam file, artinya akan dieksekusi juga sebanyak lima kali
afterEach(function)Function akan dieksekusi setelah unit test selesai, jika terdapat lima unit test dalam file, artinya akan dieksekusi juga sebanyak lima kali
beforeEach(() => {
console.log("Befor Each");
});

afterEach(() => {
console.log("After Each");
});
  • Namun kadang-kadang, kita ingin membuat kode yang hanya dieksekusi sekali saja dalam sebuah file unit test
  • Sekali sebelum semua unit test Dan sekali setelah semua unit test
  • Jest juga menyediakan fitur tersebut
FunctionKeterangan
beforeAll(function)Function akan dieksekusi sekali sebelum semua unit test berjalan di file unit test
afterAll(function)Function akan dieksekusi sekali setelah semua unit test selesai di file unit test
beforeAll(() => {
console.log("Before All");
});

afterAll(() => {
console.log("After All");
});

Scoping

  • Saat kita menggunakan Setup Function, secara default akan dieksekusi pada setiap test() function yang terdapat di file unit test
  • Jest memiliki fitur scoping atau grouping, dimana kita bisa membuat group unit test menggunakan function describe()
  • Setup Function yang dibuat di dalam describe() hanya digunakan untuk unit test di dalam describe() tersebut
  • Namun Setup Function diluar describe() secara otomatis juga digunakan di dalam describe()
beforeAll(() => console.log("Before All Outer"));
afterAll(() => console.log("After All Outer"));
beforeEach(() => console.log("Befor Each Outer"));
afterEach(() => console.log("After Each Outer"));

test("Test Outer", () => console.log("Test Outer"));

describe("Inner", () => {
beforeAll(() => console.log("Before All Inner"));
afterAll(() => console.log("After All Inner"));
beforeEach(() => console.log("Befor Each Inner"));
afterEach(() => console.log("After Each Inner"));

test("Test Inner", () => console.log("Test Inner"));

// decribe didalam describe
describe("Inner", () => {
beforeAll(() => console.log("Before All Inner"));
afterAll(() => console.log("After All Inner"));
beforeEach(() => console.log("Befor Each Inner"));
afterEach(() => console.log("After Each Inner"));

test("Test Inner", () => console.log("Test Inner"));
});
});

Code Coverage

  • Saat kita membuat unit test, kadang kita ingin tahu apakah semua kode kita sudah tercakupi dengan semua skenario unit test kita atau belum
  • Jest memiliki fitur yang bernama Code Coverage, dengan ini, kita bisa melihat kode mana yang sudah tercakupi dengan unit test, dan mana yang belum
  • Praktek ini merupakan salah satu best practice dengan menentukan jumlah persentase kode yang harus tercakupi oleh unit test, misal 80%

Menggunakan Fitur Code Coverage

{
// ...
"jest": {
"maxConcurrency": 3,
"verbose": true,
"transform": {
"^.+\\.[t|j]sx?$": "babel-jest"
},
"collectCoverage": true
}
}

Folder Coverage

  • Jest Code Coverage secara otomatis membuat folder coverage di project kita
  • Jangan lupa untuk meng-ignore folder tersebut agar tidak ter commit ke project kita
  • Folder coverage tersebut berisi laporan Code Coverage berupa file html yang bisa kita lihat dengan mudah

Coverage Threshold

  • Kadang ada kalanya kita ingin memastikan persentase Code Coverage, hal ini agar programmer dalam project pasti membuat unit test dengan baik
  • Jest memiliki fitur untuk menentukan Coverage Threshold dengan persentase, dimana jika Threshold nya dibawah persentase yang sudah ditentukan, secara otomatis maka unit test akan gagal
  • Kita bisa tambahkan konfigurasi coverageThreshold

Jenis Code Coverage

JenisKeterangan
branchesAlur program
functionsFunction
linesBaris
statementsStatement
{
// ...
"jest": {
// ...
"coverageThreshold": {
"global": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
}
// ...
}
}

Collect Coverage

{
//...
"jest": {
// ...
"collectCoverageFrom": ["src/**/*.{js,jsx}", "!vendor/**/*.{js,jsx}"]
}
}

It Function

  • Sebelumnya untuk membuat unit test, kita menggunakan function test()
  • Di Jest, terdapat alias untuk function test(), yaitu it()
  • Sebenarnya tidak ada bedanya dengan function test(), hanya saja, kadang ada programmer yang lebih suka menggunakan function it() agar unit test yang dibuat mirip dengan cerita ketika dibaca kodenya
describe("...", () => {
it("should ...", () => {
// code
});
it("should ...", () => {
// code
});
});

Skip Function

  • Saat membuat unit test, lalu kita mendapatkan masalah di salah satu unit test, kadang kita ingin meng-ignore unit test tersebut terlebih dahulu
  • Kita tidak perlu menambahkan komentar pada unit test tersebut
  • Kita bisa menggunakan skip function, yang secara otomatis akan menjadikan unit test tersebut ter-ignore dan tidak akan di eksekusi

Detail : https://jestjs.io/docs/api#testskipname-fn

test("test 01", () => console.log("test 01"));
test("test 02", () => console.log("test 02"));
test.skip("test 03", () => console.log("test 03")); // code ini tidak akan dieksekusi oleh jest
test("test 04", () => console.log("test 04"));
test("test 05", () => console.log("test 05"));

Only Function

  • Ketika kita melakukan proses debugging di unit test di dalam sebuah file yang unit test nya banyak, kadang kita ingin fokus ke unit test tertentu
  • Jika kita menggunakan skip unit test yang lain, maka akan sulit jika terlalu banyak
  • Kita bisa menggunakan Only Function, untuk memaksa dalam file tersebut, hanya unit test yang ditandai dengan Only yang di eksekusi

Detail : https://jestjs.io/docs/api#testonlyname-fn-timeout

test("test 01", () => console.log("test 01"));
test("test 02", () => console.log("test 02"));
test.only("test 03", () => console.log("test 03")); // hanya code ini yang dieksekusi oleh jest
test("test 04", () => console.log("test 04"));
test("test 05", () => console.log("test 05"));

Each Function

  • Salah satu kesalahan yang biasa dilakukan adalah membuat unit test yang duplicate
  • Biasanya alasan melakukan duplicate unit test, hanya karena data yang di test nya saja berbeda
  • Each Function memungkinkan kita menggunakan data dalam bentuk array, yang akan di iterasi ke dalam kode unit test yang sama

Detail : https://jestjs.io/docs/api#testeachtablename-fn-timeout

const sumAll = (numbers) => {
let total = 0;
for (const number of numbers) {
total += number;
}
return total;
};

const table = [
[[], 0],
[[10], 10],
[[10, 10], 20],
[[10, 10, 10], 30],
[[10, 10, 10, 10], 40],
];

test.each(table)("test sumAll(%s) should result %i", (input, expected) => {
expect(sumAll(input)).toBe(expected);
});

Object Sebagai Data

  • Kadang, saat menggunakan data Array, jika terlalu banyak parameternya, maka akan membingungkan
  • Each Function juga bisa menggunakan data Object, namun kita perlu melakukan destructuring
const sumAll = (numbers) => {
let total = 0;
for (const number of numbers) {
total += number;
}
return total;
};

const table = [
{ inputs: [10], expected: 10 },
{ inputs: [10, 10], expected: 20 },
{ inputs: [10, 10, 10], expected: 30 },
{ inputs: [10, 10, 10, 10], expected: 40 },
];

test.each(table)("test sumAll($inputs) should result $expected", ({ inputs, expected }) => {
expect(sumAll(inputs)).toBe(expected);
});

Concurrent Test

  • Secara default, semua unit test akan dijalankan secara sequential, dan unit test selanjutnya akan dijalankan ketiak unit test sebelumnya telah selesai
  • Jest juga mendukung concurrent unit test, dimana kita bisa menandai sebuah unit test agar jalan secara concurrent atau async sehingga tidak perlu ditunggu

Deatail : https://jestjs.io/docs/api#testconcurrentname-fn-timeout

const sayName = (name) => {
if (name) {
return `Hello ${name}`;
} else {
throw new Error("name is required!");
}
};

test.concurrent("concurrent 1", async () => {
await expect(sayHelloAsync("Joko")).resolves.toBe("Hello Joko");
});
test.concurrent("concurrent 2", async () => {
await expect(sayHelloAsync("Joko")).resolves.toBe("Hello Joko");
});
test.concurrent("concurrent 3", async () => {
await expect(sayHelloAsync("Joko")).resolves.toBe("Hello Joko");
});

Membatasi Concurrent

Kita bisa membatasi berapa banyak concurrent test yang berjalan dengan cara menambahkan konfigurasi di Jest nya

Detail : https://jestjs.io/docs/configuration#maxconcurrency-number

{
// ...
"jest": {
// ...
"maxConcurrency": 3
}
}

Todo Function

  • Gunakan Todo Function ketika kita berencana membuat unit test, namun dilakukan
  • Todo Function akan ditampilkan sebagai summary ketika kita menjalankan unit test, untuk mengingatkan kita

Detail : https://jestjs.io/docs/api#testtodoname

test.todo("should create test fror sumAll() big number");
test.todo("should create test fror sumAll() negative number");

Failing Function

  • Dalam membuat unit test, jangan hanya membuat skenario sukses
  • Kadang kita juga perlu membuat skenario gagal, atau ekspektasi kita gagal, contoh misal ketika kita mengirim data tidak valid, maka kita berharap kalo kode nya terjadi error
  • Pada kasus ini, Jest menyediakan fitur Failing Function Detail : https://jestjs.io/docs/api#testfailingname-fn-timeout
const sayName = (name) => {
if (name) {
return `Hello ${name}`;
} else {
throw new Error("name is required!");
}
};

test("should be succes func sayName()", () => {
expect(sayName("Joko")).toBe("Hello Joko");
});

test.failing("should be error no normal test", () => {
sayName();
});

test("should be failed normal test", () => {
expect(() => sayName()).toThrow();
});

Mock

Coming Soon...