Menulis Kode yang Dapat Diuji dalam JavaScript: Tinjauan Singkat

Diterbitkan: 2022-03-11

Baik kami menggunakan Node yang dipasangkan dengan kerangka pengujian seperti Mocha atau Jasmine, atau menjalankan pengujian yang bergantung pada DOM di browser tanpa kepala seperti PhantomJS, opsi kami untuk pengujian unit JavaScript sekarang lebih baik dari sebelumnya.

Namun, ini tidak berarti kode yang kami uji semudah kami menggunakan alat! Mengatur dan menulis kode yang mudah diuji membutuhkan upaya dan perencanaan, tetapi ada beberapa pola, yang terinspirasi oleh konsep pemrograman fungsional, yang dapat kita gunakan untuk menghindari kesulitan ketika tiba saatnya untuk menguji kode kita. Pada artikel ini, kita akan membahas beberapa tip dan pola yang berguna untuk menulis kode yang dapat diuji dalam JavaScript.

Pisahkan Logika Bisnis dan Logika Tampilan

Salah satu tugas utama aplikasi browser berbasis JavaScript adalah mendengarkan peristiwa DOM yang dipicu oleh pengguna akhir, dan kemudian menanggapinya dengan menjalankan beberapa logika bisnis dan menampilkan hasilnya di halaman. Sangat menggoda untuk menulis fungsi anonim yang melakukan sebagian besar pekerjaan tepat di tempat Anda menyiapkan pendengar acara DOM Anda. Masalah yang ditimbulkannya adalah Anda sekarang harus mensimulasikan peristiwa DOM untuk menguji fungsi anonim Anda. Ini dapat membuat overhead baik dalam baris kode maupun waktu yang diperlukan untuk menjalankan pengujian.

Sebagai gantinya, tulis fungsi bernama dan berikan ke event handler. Dengan begitu Anda dapat menulis tes untuk fungsi bernama secara langsung dan tanpa melompati rintangan untuk memicu peristiwa DOM palsu.

Ini berlaku untuk lebih dari DOM. Banyak API, baik di browser maupun di Node, dirancang untuk mengaktifkan dan mendengarkan peristiwa atau menunggu jenis pekerjaan asinkron lainnya untuk diselesaikan. Aturan praktisnya adalah jika Anda menulis banyak fungsi panggilan balik anonim, kode Anda mungkin tidak mudah untuk diuji.

 // hard to test $('button').on('click', () => { $.getJSON('/path/to/data') .then(data => { $('#my-list').html('results: ' + data.join(', ')); }); }); // testable; we can directly run fetchThings to see if it // makes an AJAX request without having to trigger DOM // events, and we can run showThings directly to see that it // displays data in the DOM without doing an AJAX request $('button').on('click', () => fetchThings(showThings)); function fetchThings(callback) { $.getJSON('/path/to/data').then(callback); } function showThings(data) { $('#my-list').html('results: ' + data.join(', ')); }

Gunakan Panggilan Balik atau Janji dengan Kode Asinkron

Dalam contoh kode di atas, fungsi fetchThings refactored kami menjalankan permintaan AJAX, yang melakukan sebagian besar pekerjaannya secara asinkron. Ini berarti kita tidak dapat menjalankan fungsi dan menguji apakah fungsi itu melakukan semua yang kita harapkan, karena kita tidak akan tahu kapan itu selesai berjalan.

Cara paling umum untuk mengatasi masalah ini adalah dengan melewatkan fungsi panggilan balik sebagai parameter ke fungsi yang berjalan secara asinkron. Dalam pengujian unit Anda, Anda dapat menjalankan pernyataan Anda dalam panggilan balik yang Anda lewati.

Ilustrasi: Menggunakan panggilan balik sebagai parameter dalam pengujian unit

Cara lain yang umum dan semakin populer untuk mengatur kode asinkron adalah dengan Promise API. Untungnya, $.ajax dan sebagian besar fungsi asinkron jQuery lainnya sudah mengembalikan objek Promise, jadi banyak kasus penggunaan umum sudah dibahas.

 // hard to test; we don't know how long the AJAX request will run function fetchData() { $.ajax({ url: '/path/to/data' }); } // testable; we can pass a callback and run assertions inside it function fetchDataWithCallback(callback) { $.ajax({ url: '/path/to/data', success: callback, }); } // also testable; we can run assertions when the returned Promise resolves function fetchDataWithPromise() { return $.ajax({ url: '/path/to/data' }); }

Hindari Efek Samping

Tulis fungsi yang mengambil argumen dan mengembalikan nilai hanya berdasarkan argumen tersebut, seperti memasukkan angka ke dalam persamaan matematika untuk mendapatkan hasil. Jika fungsi Anda bergantung pada beberapa status eksternal (properti instance kelas atau konten file, misalnya), dan Anda harus menyiapkan status tersebut sebelum menguji fungsi Anda, Anda harus melakukan lebih banyak penyiapan dalam pengujian Anda. Anda harus percaya bahwa kode lain yang dijalankan tidak mengubah status yang sama.

Ilustrasi: Efek cascading yang disebabkan oleh keadaan eksternal.

Dalam nada yang sama, hindari menulis fungsi yang mengubah keadaan eksternal (seperti menulis ke file atau menyimpan nilai ke database) saat sedang berjalan. Ini mencegah efek samping yang dapat memengaruhi kemampuan Anda untuk menguji kode lain dengan percaya diri. Secara umum, yang terbaik adalah menjaga efek samping sedekat mungkin dengan tepi kode Anda, dengan "area permukaan" sesedikit mungkin. Dalam kasus kelas dan instance objek, efek samping metode kelas harus dibatasi pada status instance kelas yang sedang diuji.

 // hard to test; we have to set up a globalListOfCars object and set up a // DOM with a #list-of-models node to test this code function processCarData() { const models = globalListOfCars.map(car => car.model); $('#list-of-models').html(models.join(', ')); } // easy to test; we can pass an argument and test its return value, without // setting any global values on the window or checking the DOM the result function buildModelsString(cars) { const models = cars.map(car => car.model); return models.join(','); }

Gunakan Injeksi Ketergantungan

Salah satu pola umum untuk mengurangi penggunaan status eksternal suatu fungsi adalah injeksi ketergantungan - melewatkan semua kebutuhan eksternal fungsi sebagai parameter fungsi.

 // depends on an external state database connector instance; hard to test function updateRow(rowId, data) { myGlobalDatabaseConnector.update(rowId, data); } // takes a database connector instance in as an argument; easy to test! function updateRow(rowId, data, databaseConnector) { databaseConnector.update(rowId, data); }

Salah satu manfaat utama menggunakan injeksi ketergantungan adalah Anda dapat meneruskan objek tiruan dari pengujian unit Anda yang tidak menyebabkan efek samping nyata (dalam hal ini, memperbarui baris basis data) dan Anda dapat menyatakan bahwa objek tiruan Anda telah ditindaklanjuti dengan cara yang diharapkan.

Berikan Setiap Fungsi Satu Tujuan

Pisahkan fungsi panjang yang melakukan beberapa hal menjadi kumpulan fungsi tujuan tunggal yang pendek. Ini membuatnya jauh lebih mudah untuk menguji bahwa setiap fungsi melakukan bagiannya dengan benar, daripada berharap yang besar melakukan semuanya dengan benar sebelum mengembalikan nilai.

Dalam pemrograman fungsional, tindakan merangkai beberapa fungsi tujuan tunggal bersama-sama disebut komposisi. Underscore.js bahkan memiliki fungsi _.compose , yang mengambil daftar fungsi dan mengikatnya bersama-sama, mengambil nilai balik dari setiap langkah dan meneruskannya ke fungsi berikutnya secara berurutan.

 // hard to test function createGreeting(name, location, age) { let greeting; if (location === 'Mexico') { greeting = '!Hola'; } else { greeting = 'Hello'; } greeting += ' ' + name.toUpperCase() + '! '; greeting += 'You are ' + age + ' years old.'; return greeting; } // easy to test function getBeginning(location) { if (location === 'Mexico') { return 'Hola'; } else { return 'Hello'; } } function getMiddle(name) { return ' ' + name.toUpperCase() + '! '; } function getEnd(age) { return 'You are ' + age + ' years old.'; } function createGreeting(name, location, age) { return getBeginning(location) + getMiddle(name) + getEnd(age); }

Jangan Mutasi Parameter

Dalam JavaScript, array dan objek dilewatkan dengan referensi daripada nilai, dan mereka bisa berubah. Ini berarti bahwa ketika Anda meneruskan objek atau larik sebagai parameter ke dalam suatu fungsi, baik kode maupun fungsi yang Anda lewati objek atau lariknya memiliki kemampuan untuk mengubah contoh yang sama dari larik atau objek itu di memori. Ini berarti bahwa jika Anda menguji kode Anda sendiri, Anda harus percaya bahwa tidak ada fungsi yang dipanggil kode Anda yang mengubah objek Anda. Setiap kali Anda menambahkan tempat baru dalam kode Anda yang mengubah objek yang sama, semakin sulit untuk melacak seperti apa objek itu seharusnya, membuatnya lebih sulit untuk diuji.

Ilustrasi: Parameter yang berubah dapat menyebabkan masalah

Sebagai gantinya, jika Anda memiliki fungsi yang mengambil objek atau larik, buatlah itu bertindak pada objek atau larik itu seolah-olah itu hanya-baca. Buat objek atau larik baru dalam kode dan tambahkan nilai berdasarkan kebutuhan Anda. Atau, gunakan Garis Bawah atau Lodash untuk mengkloning objek atau larik yang diteruskan sebelum mengoperasikannya. Lebih baik lagi, gunakan alat seperti Immutable.js yang membuat struktur data hanya-baca.

 // alters objects passed to it function upperCaseLocation(customerInfo) { customerInfo.location = customerInfo.location.toUpperCase(); return customerInfo; } // sends a new object back instead function upperCaseLocation(customerInfo) { return { name: customerInfo.name, location: customerInfo.location.toUpperCase(), age: customerInfo.age }; }

Tulis Tes Anda Sebelum Kode Anda

Proses penulisan unit test sebelum kode yang mereka uji disebut test driven development (TDD). Banyak pengembang menganggap TDD sangat membantu.

Dengan menulis tes Anda terlebih dahulu, Anda dipaksa untuk memikirkan API yang Anda ekspos dari sudut pandang pengembang yang menggunakannya. Ini juga membantu memastikan Anda hanya menulis kode yang cukup untuk memenuhi kontrak yang diberlakukan oleh pengujian Anda, daripada merekayasa solusi yang terlalu rumit yang tidak perlu.

Dalam praktiknya, TDD adalah disiplin yang mungkin sulit dilakukan untuk semua perubahan kode Anda. Tetapi ketika tampaknya pantas untuk dicoba, ini adalah cara yang bagus untuk menjamin Anda menjaga agar semua kode dapat diuji.

Bungkus

Kita semua tahu ada beberapa jebakan yang sangat mudah jatuh saat menulis dan menguji aplikasi JavaScript yang kompleks. Tapi semoga dengan tips ini, dan mengingat untuk selalu menjaga kode kita sesederhana dan sefungsional mungkin, kita dapat menjaga cakupan pengujian kita tetap tinggi dan kompleksitas kode secara keseluruhan rendah!

Terkait:
  • 10 Kesalahan Paling Umum Dilakukan Pengembang JavaScript
  • Kebutuhan akan Kecepatan: Retrospektif Tantangan Pengkodean JavaScript Toptal