evilfactorylabsarchive / blog

blog by evilfactory team
https://blog.evilfactory.id
Creative Commons Attribution Share Alike 4.0 International
5 stars 0 forks source link

Menulis Test: Retrospektif & Fundamental #10

Open faultables opened 5 years ago

faultables commented 5 years ago

Sebelumnya, pemikiran gue terhadap "menulis test" selalu ke arah teknikal:

Setelah mengerti bagian "permukaan" tentang menulis test (disisi teknikal), baru kefikiran di bagian fundamental tentang menulis test.

Mengapa menulis test?

Well, singkatnya, agar kode yang ada, berjalan sesuai yang diharapkan. Jika kita membuat test seperti ini:

const { capitalize } = require('./lib')

test('it should capitalize the word', () => {
  const testWord = capitalize('hello world')
  const expectedWord = 'Hello world'

  expect(testWord).toBe(expectedWord)
})

Maka behavior dari salah satu fungsi yang ada di file lib.js, harus berjalan sesuai dengan test yang sudah dibuat.

Oke oke, gimana dengan TDD? Yang katanya, nulis test (kode) dulu baru nulis kode (beneran). Yang katanya menggunakan konsep: Red -> Green -> Refactor?

No problem. Baca ini untuk "Bagian kontra" terhadap TDD yang ditulis oleh DHH yang seharusnya kalian sudah tau siapa dia.

Ingin TDD/enggak, yang salah menurut gue adalah bila tidak menulis test. Alias, membiarkan "pengguna akhir" yang melakukan testing terhadap kode yang kita buat. Beruntung bila di log, jika tidak?

My old-thought is possibly wrong

Dulu pernah berfikir––dan di share juga––tentang: "test apa sih yang harus saya tulis?", disitu gue jawab: Semuanya. Maksudnya, ya semuanya. Kalau kalian membuat kode, misal, kode untuk aplikasi sosial media, semua karakteristik yang ada diaplikasi tersebut, harus di test. Registrasi, Autorisasi, dsb.

Alias, terlalu luas.

Bahkan, gue gak menyebutkan jenis test nya apa dan cakupannya apa saja dan sampai mana saja.

Setelah mereview kode orang, melihat-lihat kode orang, dan melakukan testing kode orang, ada beberapa insight yang gue dapet seputar alasan menulis test dalam bagian "fundamental".

Mari kita bahas dari yang paling kecil; low-effort, dan murah yakni unit test, sampai ke yang cakupannya lebih luas, high-effort, dan mahal (dari sisi waktu) yakni e2e test.

Studi Kasus: Unit Test

Unit Test merupakan testing dari unit yang paling terkecil. Jika kamu membuat sebuah "fungsi" untuk melakukan "kapitalisasi" terhadap suatu text, maka fungsi tersebut harus masuk cakupan unit test.

Unit terkecil apa saja yang harus di test? IMO, semua public API. Jika fungsi TextTransformer memiliki public API misalnya: capitalizeText, truncateText , dan slugifyText, maka api-api tersebut yang harus ditest.

Ambil contoh, salah satu teman kita membuat library bernama Edotensei, yang memiliki fitur utama "Simple Load HTML (Assets/Resources) on the fly (Browser)".

Fungsi tersebut berguna untuk, misal, bila kita butuh "jquery.min.js", oh fuck, ganti, bila kita butuh "prism.min.js" yang dimuat via CDN (dan karena "prismJS" ini adalah sebuah syntax highlighter tools, yang diasumsikan tidak semua halaman membutuhkan library ini), maka kita bisa memuat prism.min.js tersebut on demand, on runtime. Hanya dihalaman tertentu saja.

Oke menarik, mari kita lihat apa saja yang bisa dilakukan oleh library ini.

Testing the behavior(s)

Dilihat dari contoh, ada 2 public API yang bisa digunakan: add dan remove. Mari kita lihat "karakteristik" apa saja yang ada di method tersebut

Silahkan kalian bisa simpulkan bahwa "karakteristik utama" adalah dibagian "describe", dan "karakteristik umum dari yang utama tersebut" ada dibagian "test".

Oke cool, semua test berhasil dan tidak ada yang gagal. Namun ada bagian yang menarik disini: Uncovered Line #s. Yang akan kita bahas di subtopik dibawah

Code Coverage

Dari hasil pengujian diatas, semua karakteristik yang telah dijanjikan oleh library tersebut sudah teruji, battle-tested for short. Namun ada bagian menarik dari hasil coverage diatas: Ada 3 baris yang tidak ter-"coverage". Baris 45, 158, dan 170.

Sebelum lebih dalam ke Code Coverage, ada beberapa hal mengapa reporter menganggap baris tersebut ke "Uncovered Line":

Fikirkan tentang membuat sebuah function seperti ini (misal):

return (fileType === 'js' &&
  console.log('This is JavaScript!')
) || console.log('what the fuck is that?')

Bagian console.log('This is JavaScript!') tidak akan pernah terpanggil, bila nilai dari fileType bukan js. Begitupula dengan console.log('what the fuck is that?') yang tidak akan pernah terpanggil bila nilai fileType adalah js.

Dilihat dari kasus test diatas, bagian yang tidak ter-cover oleh test adalah:

170] onLoad &&
171]   typeof onload === 'function' &&
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
172]     Object.assign(element,  {onload: onLoad})
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
173]
173] document[type ===  'css'  ?  'head'  :  'body'].appendChild(element)

Karena mungkin, nilai onLoad selalu false. Jika melihat contoh diatas, kode nya sama seperti ini:

if (onLoad) {
  if (typeof onload === 'function') {
    Object.assign(element, { onload: onLoad })
  }
}

document[type ===  'css'  ?  'head'  :  'body'].appendChild(element)

Nah ada 2 kemungkinan dan 2 level disini:

Jika onLoad bernilai true

Nah PR nya (halo pak @ri7nz), jika kita sudah membuat test bila nilai onLoad tersebut true, apakah kita juga sudah membuat test bila nilai onLoad tersebut false?

Karena mengapa? Coverage merasa akan ada "kemungkinan" bahwa mungkin Object.assign me-"return" sesuatu, sehingga baris ke 173 tidak pernah tereksekusi. Well, Object.assign() return nya object kan bukan void ?

Who knows. This is why writing test is important :))

Mungkin ada 4 cara untuk menyelesaikan masalah diatas:

  1. Mock methods Object.assign
  2. Jangan pernah panggil Object.assign() di kondisi
  3. Buat test ketika onLoad bernilai false
  4. /* istanbul ignore next */ di baris (targetRootBranch) - 1 alias 169 :)

You choose. Jangan jadikan 100% Code Coverage sebagai bottleneck, number is just a number, right?

Studi Kasus: Integration Test

Oke, meskipun beberapa orang ada yang berpendapat bahwa "Unit Test" bisa untuk men-test Private API, dan Integration Test lah yang seharusnya men-test public API, but whatever.

Perbedaan paling jelas dari Unit Test adalah "testing sesuatu yang kecil dan ter-isolasi", dan Integration Test adalah "testing bagaimana sesuatu dapat bekerja dengan lancar terhadap sesuatu", dan, yes, with less mocking as possible.

Gue ambil contoh lagi dari module nya pak @ri7nz (uhuy) yang bernama service-worker-management. Meskipun WIP, tapi sudah ada test yang bisa dilihat.

Bila kita melihat file tests/index.js nya:

import SWManagement from "../src/index";
import SWOnTheFly from "../src/ServiceWorkerOnTheFly";

const makeServiceWorkerEnv = require("service-worker-mock");
const makeFetchMock = require("service-worker-mock/fetch");
const spyRegister = jest.spyOn(SWOnTheFly.prototype, "register");

test("Test Register Service Worker", () => {
  SWManagement.register("service-worker.js");
  expect(spyRegister).toHaveBeenCalled();
});

Di baris ke 2-3 ada 2 mock yang dipanggil/dibungkus: mock untuk serviceWorker, dan mock untuk melakukan fetch() dari serviceWorker.

Kenapa meggunakan mock? Well, karena, kita tidak berinteraksi langsung dengan "komponen lain" tersebut. Melainkan, kita berinteraksi dengan "komponen lain" tersebut, seolah-olah kita benar-benar berinteraksi dengan "komponen lain" tersebut.

Singkatnya, lo melakukan testing "ngobrol sama fariz pakai bahasa sunda", yang sebenarnya, lo cuma ngobrol sama "seseorang seperti fariz, yang berbicara bahasa sunda seperti fariz".

Okok kenapa gak "berinteraksi langsung"? Hmm, mungkin karena (salah satunya) serviceWorker hanya tersedia di browser?

Inilah mengapa Integration Test lebih mahal dari Unit Test, karena pertama kita harus tau dulu "behavior" komponen yang akan kita integrasikan, yang kedua adalah tetap menjaga agar unit test kita tetap berhasil, meskipun mungkin ada beberapa kode yang harus diubah.

Pentingnya Unit Test?

Untuk meng-cover test yang tidak ter-cover oleh Unit Test.

Bagaimana kita bisa tau bahwa kita berhasil "me-register" serviceWorker di browser? Bagaimana bila ternyata browser tidak mendukung serviceWorker namun tetap mengeksekusi new serviceWorker()? Gagal. And yes, it's runtime error. Writing testing is about to avoid runtime error as much as possible, right?

Studi Kasus: e2e Testing

Yang paling mahal, dari sisi waktu dan sumber daya. Karena kita melakukan testing dengan "real data" dan "real target", dan tetap menjaga Unit & Integration test kita pass.

Studi kasusnya yaa mungkin bisa kalian sendiri yang melakukannya. Kunjungi situs yang kalian sukai/gunakan aplikasi favorit kalian, apakah ada error? Gak ada? Pass. Ada? Fail.

Namun kesalahan, bukan hanya terjadi pada program saja. Kita juga, sebagai manusia, pengguna. Human error. Mungkin human error terjadi karena "kurang baiknya" proses penyaluran informasi sehingga "pengguna" salah mengartikan sesuatu.

Misal, Twitter. Bila nge-tweet harus kurang dari 240 karakter dianggap bug oleh pengguna, maka itu (IMO) masuk ke human error. Kenapa? Karena Twitter hanya memperbolehkan nge-twit maksimal 240 karakter (17/05/2019).

Mengapa pengguna menganggapnya "bug" yang padahal, ehm, sebuah fitur? Karena mungkin, tidak ada informasi yang jelas mengenai "batasan" tersebut.

Disini, E2E testing bisa membantu. Karena kita berinteraksi dengan "manifest" asli, entah di staging ataupun production. Yang artinya:

Bedanya dengan Unit/Integration?

Yang berarti, kita perlu tau fungsionalitas (kalau bahasa agile nya "user story") dari sesuatu tersebut. Identifier tombol, identifier "pesan", identifier text field, dsb. Wasting time, right?

Dan perlu "resources" lebih, seperti: Berbagai browser (Chrome, FF, Safari, dsb), Devices (desktop, mobile, low-end devices), atau bahkan "kondisi jaringan" seperti untuk pengguna Wi-Fi, jaringan 3g, dsb.

Bayangin stress nya ketika di production, ternyata ada beberapa user yang tombol nya gak ke disable, karena mungkin, well, "latensi ketika melakukan proses validasi twit" (browser masih memproses "validasi" tapi user sudah menekan tombol "Tweet"), karena device yang digunakan user tergolong low-end. Sedangkan di e2e testing berhasil?

Gue cuma sekali seumur hidup menulis Integration Testing, baru sampai skop authentikasi, yang ribet nya minta ampun karena:

Rangkuman

Selain berguna untuk mengetahui "what the fuck my code is doing", menulis test juga berguna untuk melakukan optimasi program (less code it means less bytes & less execution, right?), melatih cara berfikir, mengetahui lebih banyak tentang sesuatu (thanks to integration test), dan mengurangi "kesempatan" kepada user untuk melakukan testing manual terhadap sistem yang kita buat.

Pak kent beck bilang: "Make it work, make it right, make it fast.", disinilah keuntungan menulis test.

Bila dulu "mental model" gue adalah: "Test apa nih yang harus ditulis?", mungkin sekarang menjadi: "Fungsi mana nih yang harus ditest?".

Jawabannya, yaaaa pastinya: Tergantung.

Setidaknya sudah tau Apa, Mengapa, Kapan, dan Yang mana nya.

Karena "testing", adalah tentang kualitas. Tentang assurance; Jaminan, kepastian. Menulis test (oleh developer) bukan berarti kita tidak butuh QA, karena pada dasarnya, sulit berfikir menjadi seorang pengembang sekaligus menjadi pengguna secara bersamaan. Melainkan, membantu mempermudah pekerjaan QA. Team Work. Jika menulis test menambah kualitas & jaminan disisi input, maka QA menambah kualitas & jaminan disisi output. Asalkan, tidak keluar dari koridor "spesifikasi" yang sudah dirancang.

Thank you, selamat berbuka puasa untuk wilayah San Francisco dan sekitarnya.