Pembangunan berdasarkan ujian telah menjadi popular sejak beberapa tahun kebelakangan ini. Banyak pengaturcara telah mencuba teknik ini, gagal, dan membuat kesimpulan bahawa TDD tidak sepadan dengan usaha yang diperlukannya.
Sebilangan pengaturcara berpendapat bahawa, secara teori, itu adalah amalan yang baik, tetapi tidak ada cukup masa untuk benar-benar menggunakan TDD. Dan yang lain berpendapat bahawa pada dasarnya membuang masa.
Sekiranya anda merasa seperti ini, saya rasa anda mungkin tidak memahami apa sebenarnya TDD. (OK, ayat sebelumnya adalah untuk menarik perhatian anda). Terdapat buku yang sangat bagus mengenai TDD, Test Driven Development: By Contoh, oleh Kent Beck, jika anda ingin menyemaknya dan mengetahui lebih lanjut.
Dalam artikel ini saya akan membahas asas-asas Pengembangan Bergerak Uji, menangani kesalahpahaman umum mengenai teknik TDD. Artikel ini juga merupakan yang pertama dari sejumlah artikel yang akan saya terbitkan, semuanya mengenai Pengujian Bergerak.
Mengapa menggunakan TDD?
Terdapat kajian, makalah, dan perbincangan mengenai seberapa berkesan TDD. Walaupun sangat berguna untuk mempunyai beberapa nombor, saya rasa mereka tidak menjawab mengapa kita mesti menggunakan TDD di tempat pertama.
Katakan bahawa anda adalah pembangun web. Anda baru sahaja menyelesaikan ciri kecil. Adakah anda menganggap cukup untuk menguji ciri ini hanya dengan berinteraksi secara manual dengan penyemak imbas? Saya rasa tidak cukup hanya bergantung pada ujian yang dilakukan oleh pembangun secara manual. Sayangnya ini bermaksud bahagian kodnya tidak cukup baik.
Tetapi pertimbangan di atas adalah mengenai pengujian, bukan TDD itu sendiri. Jadi mengapa TDD? Jawapan ringkasnya adalah "kerana ini adalah kaedah termudah untuk mencapai kod kualiti baik dan liputan ujian yang baik".
Jawapan yang lebih panjang datang dari apa sebenarnya TDD ... Mari kita mulakan dengan peraturan.
Peraturan permainan
Uncle Bob menerangkan TDD dengan tiga peraturan:
- Anda tidak dibenarkan menulis sebarang kod pengeluaran melainkan untuk membuat lulus ujian unit yang gagal. - Anda tidak dibenarkan menulis lebih banyak ujian unit daripada yang mencukupi untuk gagal; dan kegagalan penyusunan adalah kegagalan.- Anda tidak dibenarkan menulis lebih banyak kod pengeluaran daripada cukup untuk lulus ujian satu unit yang gagal.Saya juga suka versi yang lebih pendek, yang saya dapati di sini:
- Tulis hanya satu ujian unit untuk gagal.- Tuliskan hanya kod pengeluaran yang cukup untuk membuat ujian unit gagal.Peraturan ini mudah, tetapi orang yang menghampiri TDD sering melanggar satu atau lebih peraturan tersebut. Saya mencabar anda: bolehkah anda menulis projek kecil dengan mematuhi peraturan ini? Dengan projek kecil saya bermaksud sesuatu yang nyata, bukan hanya contoh yang memerlukan seperti 50 baris kod.
Peraturan tersebut menentukan mekanisme TDD, tetapi jelas bukan semua yang perlu anda ketahui. Sebenarnya, proses penggunaan TDD sering digambarkan sebagai kitaran Merah / Hijau / Refaktor. Mari lihat apa sebenarnya.
Kitaran Refaktor Hijau Merah

Fasa merah
Dalam fasa merah, anda mesti menulis ujian mengenai tingkah laku yang akan anda laksanakan. Ya, saya menulis tingkah laku . Perkataan "test" dalam Test Driven Development adalah mengelirukan. Kita semestinya menyebutnya sebagai "Pengembangan Berteraskan Tingkah Laku". Ya, saya tahu, ada orang berpendapat bahawa BDD berbeza dengan TDD, tetapi saya tidak tahu sama ada saya setuju. Jadi dalam definisi ringkas saya, BDD = TDD.
Inilah salah tanggapan umum: "Pertama saya menulis kelas dan kaedah (tetapi tidak ada pelaksanaan), kemudian saya menulis ujian untuk menguji kaedah kelas itu". Sebenarnya tidak berfungsi seperti ini.
Mari kita mundur selangkah. Mengapakah peraturan pertama TDD menghendaki anda menulis ujian sebelum menulis sebarang kod pengeluaran? Adakah kita orang gila TDD?
Setiap fasa kitaran RGR mewakili fasa dalam kitaran hayat kod dan bagaimana anda mungkin berkaitan dengannya.
Dalam fasa merah, anda bertindak seperti anda pengguna yang menuntut dan ingin menggunakan kod yang hendak ditulis dengan cara yang paling mudah. Anda harus menulis ujian yang menggunakan sekeping kod seolah-olah sudah dilaksanakan. Lupakan pelaksanaannya! Sekiranya, dalam fasa ini, anda memikirkan bagaimana anda akan menulis kod pengeluaran, anda salah melakukannya!
Pada fasa ini, anda menumpukan perhatian untuk menulis antara muka yang bersih untuk pengguna masa depan. Ini adalah fasa di mana anda merancang bagaimana kod anda akan digunakan oleh pelanggan.
Peraturan pertama ini adalah yang paling penting dan peraturan yang menjadikan TDD berbeza daripada ujian biasa. Anda menulis ujian supaya anda kemudian dapat menulis kod pengeluaran. Anda tidak menulis ujian untuk menguji kod anda.
Mari lihat contohnya.
// LeapYear.spec.jsdescribe('Leap year calculator', () => { it('should consider 1996 as leap', () => { expect(LeapYear.isLeap(1996)).toBe(true); });});
Kod di atas adalah contoh bagaimana ujian kelihatan dalam JavaScript, menggunakan kerangka ujian Jasmine. Anda tidak perlu mengenali Jasmine - cukup untuk memahami bahawa itu it(...)
adalah ujian dan expect(...).toBe(...)
cara untuk membuat Jasmine memeriksa apakah ada yang diharapkan.
Dalam ujian di atas, saya telah memeriksa bahawa fungsi itu LeapYear.isLeap(...)
kembali true
untuk tahun 1996. Anda mungkin menganggap bahawa 1996 adalah nombor ajaib dan dengan itu merupakan amalan yang tidak baik. Bukan. Dalam kod ujian, nombor sihir adalah baik, sedangkan dalam kod pengeluaran mereka harus dielakkan.
Ujian itu sebenarnya mempunyai beberapa implikasi:
- Nama kalkulator tahun lompat adalah
LeapYear
isLeap(...)
adalah kaedah statik bagiLeapYear
isLeap(...)
mengambil nombor (dan bukan array, misalnya) sebagai argumen dan mengembalikantrue
ataufalse
.
Ini satu ujian, tetapi sebenarnya mempunyai banyak implikasi! Adakah kita memerlukan kaedah untuk mengetahui apakah tahun adalah tahun lompat, atau adakah kita memerlukan kaedah yang mengembalikan senarai tahun lompatan antara tarikh mula dan akhir? Adakah nama unsur itu bermakna? Ini adalah jenis soalan yang harus anda ingat semasa menulis ujian dalam fasa Merah.
Pada fasa ini, anda harus membuat keputusan tentang bagaimana kod tersebut akan digunakan. Anda mendasarkan ini pada apa yang sebenarnya anda perlukan pada masa ini dan bukan pada apa yang anda fikir mungkin diperlukan.
Inilah kesilapan lain: jangan tulis sekumpulan fungsi / kelas yang anda fikirkan mungkin anda perlukan. Tumpukan perhatian pada ciri yang anda laksanakan dan pada perkara yang sangat diperlukan. Menulis sesuatu yang tidak diperlukan oleh ciri ini adalah teknik berlebihan.
Bagaimana dengan pengabstrakan? Akan melihatnya kemudian, dalam fasa reaktor
Fasa hijau
This is usually the easiest phase, because in this phase you write (production) code. If you are a programmer, you do that all the time.
Here comes another big mistake: instead of writing enough code to pass the red test, you write all the algorithms. While doing this, you are probably thinking about what is the most performing implementation. No way!
In this phase, you need to act like a programmer who has one simple task: write a straightforward solution that makes the test pass (and makes the alarming red on the test report becomes a friendly green). In this phase, you are allowed to violate best practices and even duplicate code. Code duplication will be removed in the refactor phase.
But why do we have this rule? Why can’t I write all the code that is already in my mind? For two reasons:
- A simple task is less prone to errors, and you want to minimize bugs.
- You definitely don’t want to mix up code which is under testing with code that is not. You can write code that is not under testing (aka legacy), but the worst thing you can do is mixing up tested and untested code.
What about clean code? What about performance? What if writing code makes me discover a problem? What about doubts?
Performance is a long story, and is out of the scope of this article. Let’s just say that performance tuning in this phase is, most of the time, premature optimization.
The test driven development technique provides two others things: a to-do list and the refactor phase.
The refactor phase is used to clean up the code. The to-do list is used to write down the steps required to complete the feature you are implementing. It also contains doubts or problems you discover during the process. A possible to-do list for the leap year calculator could be:
Feature: Every year that is exactly divisible by four is a leap year, except for years that are exactly divisible by 100, but these centurial years are leap years if they are exactly divisible by 400.
- divisible by 4- but not by 100- years divisible by 400 are leap anyway
What about leap years in Julian calendar? And years before Julian calendar?
The to-do list is live: it changes while you are coding and, ideally, at the end of the feature implementation it will be blank.
Refactor phase
In the refactor phase, you are allowed to change the code, while keeping all tests green, so that it becomes better. What “better” means is up to you. But there is something mandatory: you have to remove code duplication. Kent Becks suggests in his book that removing code duplication is all you need to do.
In this phase you play the part of a picky programmer who wants to fix/refactor the code to bring it to a professional level. In the red phase, you’re showing off your skills to your users. But in the refactor phase, you’re showing off your skills to the programmers who will read your implementation.
Removing code duplication often results in abstraction. A typical example is when you move two pieces of similar code into a helper class that works for both the functions/classes where the code has been removed.
For example the following code:
class Hello { greet() { return new Promise((resolve) => { setTimeout(()=>resolve('Hello'), 100); }); }}class Random { toss() { return new Promise((resolve) => { setTimeout(()=>resolve(Math.random()), 200); }); }}new Hello().greet().then(result => console.log(result));new Random().toss().then(result => console.log(result));
could be refactored into:
class Hello { greet() { return PromiseHelper.timeout(100).then(() => 'hello'); }}class Random { toss() { return PromiseHelper.timeout(200).then(() => Math.random()); }}class PromiseHelper { static timeout(delay) { return new Promise(resolve => setTimeout(resolve, delay)); }}const logResult = result => console.log(result);new Hello().greet().then(logResult);new Random().toss().then(logResult);
As you can see, in order to remove thenew Promise
and setTimeout
code duplication, I created a PromiseHelper.timeout(delay)
method, which serves both Hello
and Random
classes.
Just keep in mind that you cannot move to another test unless you’ve removed all the code duplication.
Final considerations
In this section I will try to answer to some common questions and misconceptions about Test Drive Development.
- T.D.D. requires much more time than “normal” programming!
What actually requires a lot of time is learning/mastering TDD as well as understanding how to set up and use a testing environment. When you are familiar with the testing tools and the TDD technique, it actually doesn’t require more time. On the contrary, it helps keep a project as simple as possible and thus saves time.
- How many test do I have to write?
The minimum amount that lets you write all the production code. The minimum amount, because every test slows down refactoring (when you change production code, you have to fix all the failing tests). On the other hand, refactoring is much simpler and safer on code under tests.
- With Test Driven Development I don’t need to spend time on analysis and on designing the architecture.
This cannot be more false. If what you are going to implement is not well-designed, at a certain point you will think “Ouch! I didn’t consider…”. And this means that you will have to delete production and test code. It is true that TDD helps with the “Just enough, just in time” recommendation of agile techniques, but it is definitely not a substitution for the analysis/design phase.
- Should test coverage be 100%?
No. As I said earlier, don’t mix up tested and untested code. But you can avoid using TDD on some parts of a project. For example I don’t test views (although a lot of frameworks make UI testing easy) because they are likely to change often. I also ensure that there is very a little logic inside views.
- I am able to write code with very a few bugs, I don’t need testing.
You may able to to that, but is the same consideration valid for all your team members? They will eventually modify your code and break it. It would be nice if you wrote tests so that a bug can be spotted immediately and not in production.
- TDD works well on examples, but in a real application a lot of the code is not testable.
I wrote a whole Tetris (as well as progressive web apps at work) using TDD. If you test first, code is clearly testable. It is more a matter of understanding how to mock dependencies and how to write simple but effective tests.
- Tests should not be written by the developers who write the code, they should be written by others, possibly QA people.
If you are speaking about testing your application, yes it is a good idea to ask other people to test what your team did. If you are speaking about writing production code, then that’s the wrong approach.
What’s next?
This article was about the philosophy and common misconceptions of TDD. I am planning to write other articles on TDD where you will see a lot of code and fewer words. If you are interested on how to develop Tetris using TDD, stay tuned!