Pengembangan JavaScript Didorong oleh Tes dalam Praktek
() translation by (you can also view the original English article)
TDD adalah proses pengembangan berulang di mana setiap iterasi dimulai dengan menulis tes yang merupakan bagian dari spesifikasi yang diterapkan. Iterasi singkat memungkinkan lebih banyak feedback instan pada kode yang ditulis, dan keputusan desain yang buruk lebih mudah ditangkap. Dengan menulis tes sebelum kode produksi apa pun, cakupan unit test yang baik datang dengan wilayah tersebut, tetapi itu hanyalah efek samping.
Setiap beberapa minggu, kita mengunjungi kembali beberapa posting favorit pembaca dari sepanjang sejarah situs. Tutorial ini pertama kali diterbitkan pada November 2010.
Pembalikan Perkembangan
Dalam pemrograman tradisional, masalah diselesaikan dengan pemrograman sampai konsep sepenuhnya terwakili dalam kode. Idealnya, kode mengikuti beberapa pertimbangan desain arsitektur secara keseluruhan, meskipun dalam banyak kasus, mungkin terutama di dunia JavaScript, ini tidak terjadi. Gaya pemrograman ini memecahkan masalah dengan menebak-nebak kode apa yang diperlukan untuk menyelesaikannya, sebuah strategi yang dapat dengan mudah menyebabkan solusi kembung dan erat digabungkan. Jika tidak ada pengujian unit juga, solusi yang dihasilkan dengan pendekatan ini bahkan mungkin berisi kode yang tidak pernah dieksekusi, seperti logika penanganan kesalahan dan penanganan argumen "fleksibel", atau mungkin berisi kasus tepi yang belum diuji secara menyeluruh, jika diuji sama sekali.
Pengembangan yang digerakkan oleh tes mengubah siklus pengembangan menjadi terbalik. Alih-alih berfokus pada kode apa yang diperlukan untuk memecahkan masalah, pengembangan yang digerakkan tes dimulai dengan mendefinisikan tujuan. Tes unit membentuk spesifikasi dan dokumentasi untuk tindakan apa yang didukung dan dipertanggungjawabkan. Memang, tujuan TDD tidak menguji dan jadi tidak ada jaminan bahwa itu menangani mis. kasus tepi lebih baik. Namun, karena setiap baris kode diuji oleh sepotong kode sampel yang representatif, TDD cenderung menghasilkan lebih sedikit kode berlebih, dan fungsionalitas yang diperhitungkan cenderung lebih kuat. Pengembangan yang digerakkan pengujian yang tepat memastikan bahwa sistem tidak akan pernah berisi kode yang tidak dieksekusi.
Proses
Proses pengembangan yang digerakkan oleh tes adalah proses berulang di mana setiap iterasi terdiri dari empat langkah berikut:
- Tulis ujian
- Jalankan tes, perhatikan tes baru gagal
- Buat lulus ujian
- Refactor untuk menghapus duplikasi
Dalam setiap iterasi, tes adalah spesifikasinya. Setelah kode produksi yang cukup (dan tidak lebih) telah ditulis untuk lulus uji, kita selesai, dan kita dapat memperbaiki kode untuk menghapus duplikasi dan/atau meningkatkan desain, selama tes masih berlalu.
TDD Praktis: Pola Pengamat
Pola Observer (juga dikenal sebagai Publish/Subcribe, atau cukup pubsub
) adalah pola desain yang memungkinkan kita untuk mengamati keadaan suatu objek dan diberitahu ketika itu berubah. Pola ini dapat memberikan objek dengan titik ekstensi yang kuat sambil mempertahankan kopling longgar.
Ada dua peran dalam The Observer - diamati dan pengamat. Pengamat adalah objek atau fungsi yang akan diberitahukan ketika keadaan diamati diamati. Observable memutuskan kapan untuk memperbarui pengamatnya dan data apa yang disediakan untuk mereka. Observable biasanya menyediakan setidaknya dua metode publik: pubsub
, yang memberi tahu pengamatnya tentang data baru, dan pubsub
yang mengikutsertakan pengamat ke acara.
Perpustakaan yang Dapat Diobservasi
Pengembangan yang digerakkan oleh tes memungkinkan kita untuk bergerak dalam langkah yang sangat kecil bila diperlukan. Dalam contoh dunia nyata pertama ini kita akan mulai dengan langkah-langkah terkecil. Saat kita mendapatkan kepercayaan pada kode dan prosesnya, kita akan secara bertahap meningkatkan ukuran langkah-langkah ketika keadaan memungkinkannya (yaitu, kode yang akan diterapkan cukup sepele). Menulis kode dalam iterasi kecil yang sering akan membantu kita merancang API sepotong demi sepotong serta membantu membuat lebih sedikit kesalahan. Ketika kesalahan terjadi, kita akan dapat memperbaikinya dengan cepat karena kesalahan akan mudah dilacak ketika menjalankan tes setiap kali kita menambahkan beberapa baris kode.
Menyiapkan Lingkungan
Contoh ini menggunakan JsTestDriver untuk menjalankan tes. Panduan pengaturan tersedia dari situs web resmi.
Tata letak proyek awal terlihat sebagai berikut:
1 |
chris@laptop:~/projects/observable $ tree |
2 |
.
|
3 |
|-- jsTestDriver.conf |
4 |
|-- src |
5 |
| `-- observable.js |
6 |
`-- test |
7 |
`-- observable_test.js
|
File konfigurasi hanyalah konfigurasi JsTestDriver
minimal:
1 |
server: https://localhost:4224 |
2 |
|
3 |
load: |
4 |
- lib/*.js |
5 |
- test/*.js
|
Menambahkan Pengamat
Kita akan memulai proyek dengan menerapkan cara untuk menambahkan pengamat ke objek. Melakukan hal itu akan membawa kita melalui penulisan tes pertama, melihatnya gagal, lulus dengan cara paling kotor dan akhirnya refactoring itu menjadi sesuatu yang lebih masuk akal.
Tes Pertama
Tes pertama akan mencoba untuk menambahkan pengamat dengan memanggil metode addObserver
. Untuk memverifikasi bahwa ini berfungsi, kita akan berterus terang dan menganggap bahwa observable menyimpan pengamatnya dalam sebuah array dan memeriksa bahwa pengamat adalah satu-satunya item dalam array itu. Tes ini termasuk dalam test/observable_test.js
dan terlihat seperti berikut:
1 |
TestCase("ObservableAddObserverTest", { |
2 |
"test should store function": function () { |
3 |
var observable = new tddjs.Observable(); |
4 |
var observer = function () {}; |
5 |
|
6 |
observable.addObserver(observer); |
7 |
|
8 |
assertEquals(observer, observable.observers[0]); |
9 |
}
|
10 |
});
|
Menjalankan Tes dan Menontonnya Gagal
Sekilas, hasil menjalankan tes pertama kita sangat menghancurkan:
1 |
Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms) |
2 |
Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms) |
3 |
ObservableAddObserverTest.test should store function error (0.00 ms): \ |
4 |
tddjs is not defined |
5 |
/test/observable_test.js:3 |
6 |
|
7 |
Tests failed. |
Membuat Test Pass
Jangan takut! Kegagalan sebenarnya adalah hal yang baik: Ia memberi tahu di mana harus memfokuskan upaya kita. Masalah serius pertama adalah bahwa tddj tidak ada. Mari kita tambahkan objek namespace di src/observable.js
:
1 |
var tddjs = {}; |
Menjalankan tes lagi menghasilkan kesalahan baru:
1 |
E
|
2 |
Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms) |
3 |
Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms) |
4 |
ObservableAddObserverTest.test should store function error (0.00 ms): \ |
5 |
tddjs.Observable is not a constructor |
6 |
/test/observable_test.js:3 |
7 |
|
8 |
Tests failed. |
Kita dapat memperbaiki masalah baru ini dengan menambahkan konstruktor yang dapat diobservasi kosong:
1 |
var tddjs = {}; |
2 |
|
3 |
(function () { |
4 |
function Observable() {} |
5 |
|
6 |
tddjs.Observable = Observable; |
7 |
}());
|
Menjalankan tes sekali lagi membawa kita langsung ke masalah berikutnya:
1 |
E
|
2 |
Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms) |
3 |
Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms) |
4 |
ObservableAddObserverTest.test should store function error (0.00 ms): \ |
5 |
observable.addObserver is not a function |
6 |
/test/observable_test.js:6 |
7 |
|
8 |
Tests failed. |
Mari kita tambahkan metode yang hilang.
1 |
function addObserver() {} |
2 |
|
3 |
Observable.prototype.addObserver = addObserver; |
Dengan metode di tempat tes sekarang gagal di tempat array pengamat yang hilang.
1 |
E
|
2 |
Total 1 tests (Passed: 0; Fails: 0; Errors: 1) (0.00 ms) |
3 |
Firefox 3.6.12 Linux: Run 1 tests (Passed: 0; Fails: 0; Errors 1) (0.00 ms) |
4 |
ObservableAddObserverTest.test should store function error (0.00 ms): \ |
5 |
observable.observers is undefined |
6 |
/test/observable_test.js:8 |
7 |
|
8 |
Tests failed. |
Seaneh kelihatannya, sekarang saya akan mendefinisikan array pengamat di dalam metode pubsub
. Ketika tes gagal, TDD menginstruksikan kami untuk melakukan hal paling sederhana yang mungkin bisa berhasil, tidak peduli seberapa kotor rasanya. Kita akan mendapat kesempatan untuk meninjau pekerjaan setelah tes lulus.
1 |
function addObserver(observer) { |
2 |
this.observers = [observer]; |
3 |
}
|
4 |
|
5 |
Success! The test now passes: |
6 |
|
7 |
.
|
8 |
Total 1 tests (Passed: 1; Fails: 0; Errors: 0) (1.00 ms) |
9 |
Firefox 3.6.12 Linux: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (1.00 ms) |
Refactoring
Saat mengembangkan solusi saat ini, kita telah mengambil rute tercepat ke tes kelulusan. Sekarang bilah berwarna, kita dapat meninjau solusi dan melakukan refactoring yang dianggap perlu. Satu-satunya aturan dalam langkah terakhir ini adalah menjaga bar hijau. Ini berarti kita harus melakukan refactor dalam langkah-langkah kecil juga, memastikan kita tidak sengaja merusak apapun.
Implementasi saat ini memiliki dua masalah yang harus kita tangani. Tes ini membuat asumsi terperinci tentang implementasi Observable dan implementasi addObserver
di-kode untuk pengujian.
Kita akan membahas hard-coding terlebih dahulu. Untuk mengekspos solusi hard-coded, kita akan menambah tes untuk membuatnya menambahkan dua pengamat, bukan satu.
1 |
"test should store function": function () { |
2 |
var observable = new tddjs.Observable(); |
3 |
var observers = [function () {}, function () {}]; |
4 |
|
5 |
observable.addObserver(observers[0]); |
6 |
observable.addObserver(observers[1]); |
7 |
|
8 |
assertEquals(observers, observable.observers); |
9 |
}
|
Seperti yang diharapkan, tes sekarang gagal. Tes mengharapkan fungsi yang ditambahkan sebagai pengamat harus menumpuk seperti elemen apa pun yang ditambahkan ke pubsub
. Untuk mencapai ini, kita akan memindahkan instantiation array ke konstruktor dan cukup mendelegasikan addObserver
ke push metode array
:
1 |
function Observable() { |
2 |
this.observers = []; |
3 |
}
|
4 |
|
5 |
function addObserver(observer) { |
6 |
this.observers.push(observer); |
7 |
}
|
Dengan implementasi ini di tempat tes lulus lagi, membuktikan bahwa kami telah mengurus solusi hard-coded. Namun, masalah mengakses properti publik dan membuat asumsi liar tentang implementasi Observable masih menjadi masalah. pubsub
yang dapat diamati harus dapat diamati oleh sejumlah objek, tetapi tidak menarik bagi orang luar bagaimana atau di mana toko yang diamati menyimpannya. Idealnya, kita ingin dapat memeriksa dengan diamati jika pengamat tertentu terdaftar tanpa meraba-raba bagian dalamnya. Kita mencatat aroma dan melanjutkan. Nanti, kita akan kembali untuk meningkatkan tes ini.
Memeriksa Pengamat
Kita akan menambahkan metode lain ke Observable
, hasObserver, dan menggunakannya untuk menghapus beberapa kekacauan yang ditambahkan saat menerapkan addObserver
.
Tes
Metode baru dimulai dengan tes baru, dan perilaku berikutnya yang diinginkan untuk metode hasObserver
.
1 |
TestCase("ObservableHasObserverTest", { |
2 |
"test should return true when has observer": function () { |
3 |
var observable = new tddjs.Observable(); |
4 |
var observer = function () {}; |
5 |
|
6 |
observable.addObserver(observer); |
7 |
|
8 |
assertTrue(observable.hasObserver(observer)); |
9 |
}
|
10 |
});
|
Kita berharap tes ini gagal dalam menghadapi hasObserver
yang hilang, yang ternyata berhasil.
Membuat Test Pass
Sekali lagi, kita menggunakan solusi paling sederhana yang dapat lulus tes saat ini:
1 |
function hasObserver(observer) { |
2 |
return true; |
3 |
}
|
4 |
|
5 |
Observable.prototype.hasObserver = hasObserver; |
Meskipun kita tahu ini tidak akan menyelesaikan masalah kami dalam jangka panjang, ini membuat tes tetap hijau. Mencoba untuk meninjau dan refactor membuat kita tidak punya tangan kosong karena tidak ada poin yang jelas di mana kita dapat meningkatkan. Tes adalah persyaratan kita, dan saat ini mereka hanya memerlukan hasObserver
untuk mengembalikan true. Untuk memperbaikinya, kita akan memperkenalkan tes lain yang mengharapkan hasObserver
untuk return false
pengamat yang tidak ada, yang dapat membantu memaksa solusi nyata.
1 |
"test should return false when no observers": function () { |
2 |
var observable = new tddjs.Observable(); |
3 |
|
4 |
assertFalse(observable.hasObserver(function () {})); |
5 |
}
|
Tes ini gagal total, mengingat hasObserver
selalu returns true
, memaksa kita untuk menghasilkan implementasi nyata. Memeriksa apakah pengamat terdaftar adalah masalah sederhana untuk memeriksa bahwa array this.observers berisi objek yang semula diteruskan ke addObserver
:
1 |
function hasObserver(observer) { |
2 |
return this.observers.indexOf(observer) >= 0; |
3 |
}
|
Metode Array.prototype.indexOf
mengembalikan angka kurang dari 0
jika elemen tidak ada dalam array
, jadi memeriksa bahwa itu mengembalikan angka sama dengan atau lebih besar dari 0
akan memberi tahu kita jika pengamat ada.
Mengatasi Ketidakcocokan Browser
Menjalankan tes di lebih dari satu browser menghasilkan hasil yang agak mengejutkan:
1 |
chris@laptop:~/projects/observable$ jstestdriver --tests all |
2 |
...E |
3 |
Total 4 tests (Passed: 3; Fails: 0; Errors: 1) (11.00 ms) |
4 |
Firefox 3.6.12 Linux: Run 2 tests (Passed: 2; Fails: 0; Errors 0) (2.00 ms) |
5 |
Microsoft Internet Explorer 6.0 Windows: Run 2 tests \ |
6 |
(Passed: 1; Fails: 0; Errors 1) (0.00 ms) |
7 |
ObservableHasObserverTest.test should return true when has observer error \ |
8 |
(0.00 ms): Object doesn't support this property or method |
9 |
|
10 |
Tests failed.
|
Internet Explorer versi 6 dan 7 gagal tes dengan pesan kesalahan paling umum: "Objek tidak mendukung properti atau metode ini
". Ini dapat menunjukkan sejumlah masalah:
- Kita memanggil metode pada objek yang null
- Kita memanggil metode yang tidak ada
- kita mengakses properti yang tidak ada
Untungnya, dalam langkah-langkah kecil, kita tahu bahwa kesalahan harus berhubungan dengan panggilan yang baru saja ditambahkan ke indexOf
pada array
pengamat kami. Ternyata, IE 6 dan 7 tidak mendukung metode JavaScript 1.6 Array.prototype.indexOf
(yang tidak dapat benar-benar menyalahkannya, itu hanya baru-baru ini distandarisasi dengan ECMAScript 5, Desember 2009). Pada titik ini, kita memiliki tiga opsi:
- Kurangi penggunaan Array.prototype.indexOf di hasObserver, secara efektif menduplikasi fungsi asli dalam mendukung browser.
- Terapkan Array.prototype.indexOf untuk browser yang tidak mendukung. Sebagai alternatif, implementasikan fungsi pembantu yang menyediakan fungsionalitas yang sama.
- Gunakan perpustakaan pihak ketiga yang menyediakan metode yang hilang, atau metode serupa.
Salah satu dari pendekatan ini yang paling cocok untuk menyelesaikan masalah yang diberikan akan tergantung pada situasi – mereka semua memiliki pro dan kontra. Demi menjaga agar Self-Contained mandiri, kita hanya akan mengimplementasikan hasObserver
dalam hal loop di tempat panggilan indexOf
, yang secara efektif mengatasi masalah. Kebetulan, itu juga tampaknya menjadi hal paling sederhana yang mungkin dapat bekerja pada titik ini. Jika kita mengalami situasi yang sama di kemudian hari, kita akan disarankan untuk mempertimbangkan kembali keputusan kita. hasObserver
yang diperbarui terlihat sebagai berikut:
1 |
function hasObserver(observer) { |
2 |
for (var i = 0, l = this.observers.length; i < l; i++) { |
3 |
if (this.observers[i] == observer) { |
4 |
return true; |
5 |
}
|
6 |
}
|
7 |
|
8 |
return false; |
9 |
}
|
Refactoring
Dengan bilah kembali menjadi hijau, saatnya meninjau kemajuan. Kita sekarang memiliki tiga tes, tetapi dua di antaranya anehnya serupa. Tes pertama yang ditulis untuk memverifikasi kebenaran addObserver
pada dasarnya menguji hal-hal yang sama dengan tes yang ditulis untuk memverifikasi Refactoring
. Ada dua perbedaan utama antara dua tes: Tes pertama sebelumnya dinyatakan smelly, karena langsung mengakses array pengamat di dalam objek yang dapat diamati. Tes pertama menambahkan dua pengamat, memastikan keduanya ditambahkan. Kita sekarang dapat menggabungkan tes menjadi satu yang memverifikasi bahwa semua pengamat yang ditambahkan ke yang diamati benar-benar ditambahkan:
1 |
"test should store functions": function () { |
2 |
var observable = new tddjs.Observable(); |
3 |
var observers = [function () {}, function () {}]; |
4 |
|
5 |
observable.addObserver(observers[0]); |
6 |
observable.addObserver(observers[1]); |
7 |
|
8 |
assertTrue(observable.hasObserver(observers[0])); |
9 |
assertTrue(observable.hasObserver(observers[1])); |
10 |
}
|
Memberitahu Pengamat
Menambahkan pengamat dan memeriksa keberadaan mereka bagus, tetapi tanpa kemampuan untuk memberi tahu mereka tentang perubahan yang menarik, Observable tidak terlalu berguna. Saatnya menerapkan metode notifikasi.
Memastikan Bahwa Pengamat Dipanggil
Tugas paling penting yang diberitahukan melakukan adalah memanggil semua pengamat. Untuk melakukan ini, kita perlu beberapa cara untuk memverifikasi bahwa pengamat telah dipanggil setelah fakta. Untuk memverifikasi bahwa suatu fungsi telah dipanggil, kita dapat mengatur properti pada fungsi ketika itu dipanggil. Untuk memverifikasi pengujian, kita dapat memeriksa apakah properti telah disetel. Tes berikut menggunakan konsep ini dalam tes pertama untuk memberi tahu.
1 |
TestCase("ObservableNotifyTest", { |
2 |
"test should call all observers": function () { |
3 |
var observable = new tddjs.Observable(); |
4 |
var observer1 = function () { observer1.called = true; }; |
5 |
var observer2 = function () { observer2.called = true; }; |
6 |
|
7 |
observable.addObserver(observer1); |
8 |
observable.addObserver(observer2); |
9 |
observable.notify(); |
10 |
|
11 |
assertTrue(observer1.called); |
12 |
assertTrue(observer2.called); |
13 |
}
|
14 |
});
|
Untuk lulus tes, kita perlu mengulang larik pengamat dan memanggil setiap fungsi:
1 |
function notify() { |
2 |
for (var i = 0, l = this.observers.length; i < l; i++) { |
3 |
this.observers[i](); |
4 |
}
|
5 |
}
|
6 |
|
7 |
Observable.prototype.notify = notify; |
Melewati Argumen
Saat ini para pengamat dipanggil, tetapi mereka tidak diberi data apa pun. Mereka tahu sesuatu terjadi - tetapi tidak harus apa. Kita akan membuat notifikasi mengambil sejumlah argumen, cukup meneruskannya kepada setiap pengamat:
1 |
"test should pass through arguments": function () { |
2 |
var observable = new tddjs.Observable(); |
3 |
var actual; |
4 |
|
5 |
observable.addObserver(function () { |
6 |
actual = arguments; |
7 |
});
|
8 |
|
9 |
observable.notify("String", 1, 32); |
10 |
|
11 |
assertEquals(["String", 1, 32], actual); |
12 |
}
|
Tes ini membandingkan argumen yang diterima dan lulus dengan menetapkan argumen yang diterima ke variabel lokal untuk tes. Pengamat yang baru saja kita buat sebenarnya adalah mata-mata tes manual yang sangat sederhana. Menjalankan tes mengonfirmasi bahwa itu gagal, yang tidak mengejutkan karena kita saat ini tidak menyentuh argumen di dalam pemberitahuan.
Untuk lulus ujian, kita dapat menggunakan aplikasi saat memanggil pengamat:
1 |
function notify() { |
2 |
for (var i = 0, l = this.observers.length; i < l; i++) { |
3 |
this.observers[i].apply(this, arguments); |
4 |
}
|
5 |
}
|
Dengan tes perbaikan sederhana ini kembali ke hijau. Perhatikan bahwa kita mengirimkan ini sebagai argumen pertama untuk diterapkan, yang berarti bahwa pengamat akan dipanggil dengan yang dapat diamati karena ini.
Penanganan Kesalahan
Pada titik ini Observable berfungsi dan kita memiliki tes yang memverifikasi perilakunya. Namun, tes hanya memverifikasi bahwa yang dapat diamati berperilaku benar dalam menanggapi input yang diharapkan. Apa yang terjadi jika seseorang mencoba mendaftarkan objek sebagai pengamat menggantikan fungsi? Apa yang terjadi jika salah satu pengamat meledak? Itu adalah pertanyaan yang perlu uji untuk menjawab. Memastikan perilaku yang benar dalam situasi yang diharapkan adalah penting – itulah yang paling sering dilakukan objek kita. Setidaknya supaya kita bisa berharap. Namun, perilaku yang benar bahkan ketika klien melakukan kesalahan sama pentingnya untuk menjamin sistem yang stabil dan dapat diprediksi.
Menambahkan Pengamat Bogus
Implementasi saat ini secara membabi buta menerima segala jenis argumen untuk addObserver
. Meskipun implementasi kita dapat menggunakan fungsi apa pun sebagai pengamat, ia tidak dapat menangani nilai apa pun. Tes berikut ini mengharapkan yang dapat diamati untuk melemparkan pengecualian ketika mencoba untuk menambahkan pengamat yang tidak bisa dipanggil.
1 |
"test should throw for uncallable observer": function () { |
2 |
var observable = new tddjs.Observable(); |
3 |
|
4 |
assertException(function () { |
5 |
observable.addObserver({}); |
6 |
}, "TypeError"); |
7 |
}
|
Dengan melempar pengecualian saat menambahkan pengamat, kita tidak perlu khawatir tentang data yang tidak valid nanti saat kita memberi tahu pengamat. Seandainya kita memprogram berdasarkan kontrak, kita dapat mengatakan bahwa prasyarat untuk metode addObserver
adalah bahwa input harus dapat dipanggil. postcondition
adalah bahwa pengamat ditambahkan ke yang dapat diobservasi dan dijamin akan dipanggil begitu panggilan yang diobservasi memberitahukan.
Tes gagal, jadi kita menggeser fokus untuk mendapatkan bar hijau lagi secepat mungkin. Sayangnya, tidak ada cara untuk memalsukan implementasi ini – melempar pengecualian pada setiap panggilan ke addObserver
akan gagal semua tes lainnya. Untungnya, implementasinya cukup sepele:
1 |
function addObserver(observer) { |
2 |
if (typeof observer != "function") { |
3 |
throw new TypeError("observer is not function"); |
4 |
}
|
5 |
|
6 |
this.observers.push(observer); |
7 |
}
|
addObserver
sekarang memeriksa bahwa pengamat sebenarnya adalah fungsi sebelum menambahkannya ke daftar. Menjalankan tes menghasilkan perasaan sukses yang manis: Semuanya hijau.
Pengamat yang Berperilaku Tidak Baik
Observable sekarang menjamin bahwa setiap pengamat yang ditambahkan melalui addObserver
dapat dipanggil. Namun, memberi tahu masih mungkin gagal mengerikan jika pengamat melempar pengecualian. Tes selanjutnya mengharapkan semua pengamat dipanggil meskipun salah satu dari mereka melempar pengecualian.
1 |
"test should notify all even when some fail": function () { |
2 |
var observable = new tddjs.Observable(); |
3 |
var observer1 = function () { throw new Error("Oops"); }; |
4 |
var observer2 = function () { observer2.called = true; }; |
5 |
|
6 |
observable.addObserver(observer1); |
7 |
observable.addObserver(observer2); |
8 |
observable.notify(); |
9 |
|
10 |
assertTrue(observer2.called); |
11 |
}
|
Menjalankan tes mengungkapkan bahwa implementasi saat ini meledak bersama dengan pengamat pertama, menyebabkan pengamat kedua tidak dipanggil. Akibatnya, notifikasi melanggar jaminan bahwa ia akan selalu memanggil semua pengamat setelah mereka berhasil ditambahkan. Untuk memperbaiki situasi, metode yang perlu dipersiapkan untuk yang terburuk:
1 |
function notify() { |
2 |
for (var i = 0, l = this.observers.length; i < l; i++) { |
3 |
try { |
4 |
this.observers[i].apply(this, arguments); |
5 |
} catch (e) {} |
6 |
}
|
7 |
}
|
Pengecualian dibuang secara diam-diam. Adalah tanggung jawab pengamat untuk memastikan bahwa setiap kesalahan ditangani dengan benar, yang diamati hanya menangkis pengamat yang berperilaku buruk.
Mendokumentasikan Call Order
Kita telah meningkatkan kekokohan modul Observable dengan memberikannya penanganan kesalahan yang tepat. Modul ini sekarang dapat memberikan jaminan operasi selama mendapat input yang baik dan dapat pulih jika pengamat gagal memenuhi persyaratannya. Namun, tes terakhir yang ditambahkan membuat asumsi pada fitur yang tidak terdokumentasi dari yang dapat diamati: Ini mengasumsikan bahwa pengamat dipanggil dalam urutan yang ditambahkan. Saat ini, solusi ini berfungsi karena kita menggunakan array untuk mengimplementasikan daftar pengamat. Namun, jika kita memutuskan untuk mengubah ini, tes kita mungkin rusak. Jadi kita perlu memutuskan: apakah kita menolak tes untuk tidak menerima pesanan panggilan, atau apakah kita hanya menambahkan tes yang mengharapkan pesanan panggilan –dengan demikian mendokumentasikan pesanan panggilan sebagai fitur? Urutan panggilan sepertinya fitur yang masuk akal, jadi pengujian berikutnya akan memastikan Observable menjaga perilaku ini.
1 |
"test should call observers in the order they were added": |
2 |
function () { |
3 |
var observable = new tddjs.Observable(); |
4 |
var calls = []; |
5 |
var observer1 = function () { calls.push(observer1); }; |
6 |
var observer2 = function () { calls.push(observer2); }; |
7 |
observable.addObserver(observer1); |
8 |
observable.addObserver(observer2); |
9 |
|
10 |
observable.notify(); |
11 |
|
12 |
assertEquals(observer1, calls[0]); |
13 |
assertEquals(observer2, calls[1]); |
14 |
}
|
Karena implementasi sudah menggunakan array untuk pengamat, tes ini berhasil segera.
Mengamati Objek Sewenang-wenang
Dalam bahasa statis dengan warisan klasik, objek sewenang-wenang dibuat dapat diobservasi dengan mensubkelas kelas Observable. Motivasi untuk pewarisan klasik dalam kasus-kasus ini berasal dari keinginan untuk mendefinisikan mekanisme pola di satu tempat dan menggunakan kembali logika di sejumlah besar objek yang tidak terkait. Dalam JavaScript, kita memiliki beberapa opsi untuk menggunakan kembali kode di antara objek, jadi tidak perlu membatasi diri pada persaingan model warisan klasik.
Untuk membebaskan diri dari persaingan klasik yang disediakan oleh konstruktor, perhatikan contoh berikut yang mengasumsikan bahwa tddjs.observable adalah objek daripada konstruktor:
Catatan: Metode tddjs.extend
diperkenalkan di tempat lain dalam buku ini dan cukup menyalin properti dari satu objek ke objek lainnya.
1 |
|
2 |
// Creating a single observable object
|
3 |
var observable = Object.create(tddjs.util.observable); |
4 |
|
5 |
// Extending a single object
|
6 |
tddjs.extend(newspaper, tddjs.util.observable); |
7 |
|
8 |
// A constructor that creates observable objects
|
9 |
function Newspaper() { |
10 |
/* ... */
|
11 |
}
|
12 |
|
13 |
Newspaper.prototype = Object.create(tddjs.util.observable); |
14 |
|
15 |
// Extending an existing prototype
|
16 |
tddjs.extend(Newspaper.prototype, tddjs.util.observable); |
Cukup menerapkan objek yang dapat diamati sebagai satu objek menawarkan banyak fleksibilitas. Untuk sampai di sana kita perlu memperbaiki solusi yang ada untuk menyingkirkan konstruktor.
Membuat Constructor Usang
Untuk menghilangkan konstruktor, pertama-tama kita harus refactor diamati sehingga konstruktor tidak melakukan pekerjaan apa pun. Untungnya, konstruktor hanya menginisialisasi array pengamat, yang seharusnya tidak terlalu sulit untuk dihapus. Semua metode di Observable.prototype mengakses array, jadi kita perlu memastikan mereka semua bisa menangani case yang belum diinisialisasi. Untuk menguji ini, kita hanya perlu menulis satu tes per metode yang memanggil metode tersebut sebelum melakukan hal lain.
Karena kita sudah memiliki tes yang memanggil addObserver
dan hasObserver
sebelum melakukan hal lain, kita akan berkonsentrasi pada metode notify. Metode ini hanya diuji setelah addObserver
dipanggil. Tes kita berikutnya mengharapkan untuk memanggil metode ini sebelum menambahkan pengamat.
1 |
"test should not fail if no observers": function () { |
2 |
var observable = new tddjs.Observable(); |
3 |
|
4 |
assertNoException(function () { |
5 |
observable.notify(); |
6 |
});
|
7 |
}
|
Dengan tes ini di tempat kita dapat mengosongkan konstruktor:
1 |
function Observable() { |
2 |
}
|
Menjalankan tes menunjukkan bahwa semua kecuali satu sekarang gagal, semua dengan pesan yang sama: "pengamat ini tidak didefinisikan". Kita akan berurusan dengan satu metode pada satu waktu. Pertama adalah metode addObserver
:
function addObserver(observer) {
if (!this.observers) {
this.observers = [];
}
/* ... */
}
Menjalankan tes lagi mengungkapkan bahwa metode addObserver
yang diperbarui memperbaiki semua kecuali dua tes yang tidak dimulai dengan memanggilnya. Selanjutnya, kita memastikan untuk mengembalikan false langsung dari hasObserver
jika array tidak ada.
1 |
function hasObserver(observer) { |
2 |
if (!this.observers) { |
3 |
return false; |
4 |
}
|
5 |
|
6 |
/* ... */
|
7 |
}
|
Kita dapat menerapkan perbaikan yang sama persis untuk memberi tahu:
1 |
function notify(observer) { |
2 |
if (!this.observers) { |
3 |
return; |
4 |
}
|
5 |
|
6 |
/* ... */
|
7 |
}
|
Memasang Kembali Konstruktor dengan Objek
Sekarang constructor
tidak melakukan apa-apa, itu dapat dihapus dengan aman. Kita kemudian akan menambahkan semua metode secara langsung ke object
tddjs.observable
, yang kemudian dapat digunakan dengan mis. Object.create atau tddjs.extend
untuk membuat objek yang bisa diamati. Perhatikan bahwa nama tersebut tidak lagi ditulis dengan huruf besar karena tidak lagi merupakan konstruktor. Implementasi yang diperbarui berikut:
1 |
(function () { |
2 |
function addObserver(observer) { |
3 |
/* ... */
|
4 |
}
|
5 |
|
6 |
function hasObserver(observer) { |
7 |
/* ... */
|
8 |
}
|
9 |
|
10 |
function notify() { |
11 |
/* ... */
|
12 |
}
|
13 |
|
14 |
tddjs.observable = { |
15 |
addObserver: addObserver, |
16 |
hasObserver: hasObserver, |
17 |
notify: notify |
18 |
};
|
19 |
}());
|
Tentunya, melepas konstruktor menyebabkan semua tes sejauh ini rusak. Namun, memperbaikinya mudah. Yang perlu kita lakukan adalah mengganti pernyataan baru dengan panggilan ke Object.create
. Namun, sebagian besar browser belum mendukung Object.create
, sehingga kita dapat menguranginya. Karena metode ini tidak mungkin untuk ditiru dengan sempurna, kita akan memberikan versi sendiri pada object
tddjs
:
1 |
(function () { |
2 |
function F() {} |
3 |
|
4 |
tddjs.create = function (object) { |
5 |
F.prototype = object; |
6 |
return new F(); |
7 |
};
|
8 |
|
9 |
/* Observable implementation goes here ... */
|
10 |
}());
|
Dengan shim di tempatnya, kita dapat memperbarui tes dalam masalah yang akan berfungsi bahkan di browser lama. Paket tes akhir berikut:
1 |
TestCase("ObservableAddObserverTest", { |
2 |
setUp: function () { |
3 |
this.observable = tddjs.create(tddjs.observable); |
4 |
},
|
5 |
|
6 |
"test should store functions": function () { |
7 |
var observers = [function () {}, function () {}]; |
8 |
|
9 |
this.observable.addObserver(observers[0]); |
10 |
this.observable.addObserver(observers[1]); |
11 |
|
12 |
assertTrue(this.observable.hasObserver(observers[0])); |
13 |
assertTrue(this.observable.hasObserver(observers[1])); |
14 |
}
|
15 |
});
|
16 |
|
17 |
TestCase("ObservableHasObserverTest", { |
18 |
setUp: function () { |
19 |
this.observable = tddjs.create(tddjs.observable); |
20 |
},
|
21 |
|
22 |
"test should return false when no observers": function () { |
23 |
assertFalse(this.observable.hasObserver(function () {})); |
24 |
}
|
25 |
});
|
26 |
|
27 |
TestCase("ObservableNotifyTest", { |
28 |
setUp: function () { |
29 |
this.observable = tddjs.create(tddjs.observable); |
30 |
},
|
31 |
|
32 |
"test should call all observers": function () { |
33 |
var observer1 = function () { observer1.called = true; }; |
34 |
var observer2 = function () { observer2.called = true; }; |
35 |
|
36 |
this.observable.addObserver(observer1); |
37 |
this.observable.addObserver(observer2); |
38 |
this.observable.notify(); |
39 |
|
40 |
assertTrue(observer1.called); |
41 |
assertTrue(observer2.called); |
42 |
},
|
43 |
|
44 |
"test should pass through arguments": function () { |
45 |
var actual; |
46 |
|
47 |
this.observable.addObserver(function () { |
48 |
actual = arguments; |
49 |
});
|
50 |
|
51 |
this.observable.notify("String", 1, 32); |
52 |
|
53 |
assertEquals(["String", 1, 32], actual); |
54 |
},
|
55 |
|
56 |
"test should throw for uncallable observer": function () { |
57 |
var observable = this.observable; |
58 |
|
59 |
assertException(function () { |
60 |
observable.addObserver({}); |
61 |
}, "TypeError"); |
62 |
},
|
63 |
|
64 |
"test should notify all even when some fail": function () { |
65 |
var observer1 = function () { throw new Error("Oops"); }; |
66 |
var observer2 = function () { observer2.called = true; }; |
67 |
|
68 |
this.observable.addObserver(observer1); |
69 |
this.observable.addObserver(observer2); |
70 |
this.observable.notify(); |
71 |
|
72 |
assertTrue(observer2.called); |
73 |
},
|
74 |
|
75 |
"test should call observers in the order they were added": |
76 |
function () { |
77 |
var calls = []; |
78 |
var observer1 = function () { calls.push(observer1); }; |
79 |
var observer2 = function () { calls.push(observer2); }; |
80 |
this.observable.addObserver(observer1); |
81 |
this.observable.addObserver(observer2); |
82 |
|
83 |
this.observable.notify(); |
84 |
|
85 |
assertEquals(observer1, calls[0]); |
86 |
assertEquals(observer2, calls[1]); |
87 |
},
|
88 |
|
89 |
"test should not fail if no observers": function () { |
90 |
var observable = this.observable; |
91 |
|
92 |
assertNoException(function () { |
93 |
observable.notify(); |
94 |
});
|
95 |
}
|
96 |
});
|
Untuk menghindari duplikasi panggilan tddjs.create
, setiap test case memperoleh method
setUp
yang mengatur observable untuk pengujian. Metode pengujian harus diperbarui sesuai, menggantikan diamati dengan ini. Diobservasi.
Ringkasan
Melalui kutipan dari buku ini, kita memiliki pengantar lunak untuk Pengembangan Test-Driven with JavaScript. Tentu saja, API saat ini terbatas dalam kemampuannya, tetapi buku ini memperluas lebih lanjut dengan memungkinkan pengamat untuk mengamati dan memberi tahu acara khusus, seperti observable.observe(
"beforeLoad
", myObserver
).
Buku ini juga memberikan wawasan tentang bagaimana Anda dapat menerapkan TDD untuk mengembangkan kode yang mis. sangat bergantung pada manipulasi DOM dan Ajax, dan akhirnya menyatukan semua proyek sampel dalam aplikasi obrolan berbasis browser yang berfungsi penuh.
Kutipan ini didasarkan pada buku, 'Pengembangan JavaScript Didorong-Tes', yang ditulis oleh Christian Johansen, diterbitkan oleh Pearson / Addison-Wesley Professional, September 2010, ISBN 0321683919, Hak Cipta 2011 Pearson Education, Inc. Lihat di sini untuk Tabel selengkapnya Isi