Тестова розробка: що це, а що ні.

Тестова розробка стала популярною протягом останніх кількох років. Багато програмістів випробували цю техніку, не вдавшись, і дійшли висновку, що TDD не вартий зусиль, яких вона вимагає.

Деякі програмісти вважають, що теоретично це є гарною практикою, але ніколи не вистачає часу для того, щоб реально використовувати TDD. А інші вважають, що це в основному марна трата часу.

Якщо ви почуваєтесь так, я думаю, ви могли б не зрозуміти, що насправді є TDD. (Добре, попереднє речення мало привернути вашу увагу). Існує дуже хороша книга про TDD «Тест-розробник: на прикладі» Кента Бека, якщо ви хочете перевірити це та дізнатись більше.

У цій статті я розгляну основи розробки, керованої тестуванням, розглядаючи типові помилки щодо техніки TDD. Ця стаття також є першою з ряду статей, які я збираюся опублікувати, про тестування.

Навіщо використовувати TDD?

Є дослідження, статті та дискусії про те, наскільки ефективним є TDD. Незважаючи на те, що однозначно корисно мати деякі цифри, я не думаю, що вони відповідають на питання, чому нам слід використовувати TDD в першу чергу.

Скажіть, що ви веб-розробник. Ви щойно закінчили невеличку функцію. Чи вважаєте Ви досить протестувати цю функцію, просто взаємодіючи вручну з браузером? Я думаю, що недостатньо покладатися лише на тести, які розробники роблять вручну. На жаль, це означає, що частина коду недостатньо хороша.

Але розгляд вище стосується тестування, а не самого TDD. То чому TDD? Коротка відповідь: "тому що це найпростіший спосіб досягти як доброякісного коду, так і гарного охоплення тестом".

Більш довга відповідь випливає з того, що насправді є TDD ... Почнемо з правил.

Правила гри

Дядько Боб описує TDD з трьома правилами:

- Вам заборонено писати будь-який виробничий код, якщо він не робить невдалого проходження одиничного тесту. - Вам не дозволяється писати більше одиничного тесту, ніж достатньо для провалу; і помилки компіляції є помилками. - Вам заборонено писати більше виробничого коду, ніж достатньо для проходження одного невдалого модульного тесту.

Мені також подобається коротша версія, яку я знайшов тут:

- Напишіть лише достатньо модульного тесту, щоб провалитися. - Напишіть лише достатньо виробничого коду, щоб пройти невдалий модульний тест.

Ці правила прості, але люди, які звертаються до TDD, часто порушують одне або кілька з них. Я кидаю вам виклик: чи можете ви написати невеликий проект, суворо дотримуючись цих правил? Під невеликим проектом я маю на увазі щось реальне, а не просто приклад, для якого потрібно близько 50 рядків коду.

Ці правила визначають механіку TDD, але вони точно не все, що вам потрібно знати. Насправді процес використання TDD часто описується як цикл Червоний / Зелений / Рефактор. Давайте подивимось, про що йдеться.

Червоно-зелений цикл рефактора

Червона фаза

На червоній фазі вам потрібно написати тест на поведінку, яку ви збираєтесь реалізувати. Так, я писав поведінку . Слово "тест" у тестовій розробці вводить в оману. Нам слід було б назвати це “Поводницький розвиток”. Так, я знаю, деякі люди стверджують, що BDD відрізняється від TDD, але я не знаю, чи погоджуюсь я. Отже, у моєму спрощеному визначенні BDD = TDD.

Ось одна типова помилкова думка: «Спочатку я пишу клас і метод (але не реалізую), потім пишу тест для перевірки цього методу класу». Це насправді не працює таким чином.

Зробимо крок назад. Чому перше правило TDD вимагає, щоб ви написали тест, перш ніж писати будь-який фрагмент виробничого коду? Ми маньяки людей TDD?

Кожна фаза циклу RGR представляє фазу в життєвому циклі коду та те, як ви можете до нього ставитись.

На червоній фазі ви поводитесь так, ніби ви вимогливий користувач, який хоче використовувати код, який збирається написати найпростішим можливим способом. Ви повинні написати тест, який використовує фрагмент коду так, ніби він уже реалізований. Забудьте про реалізацію! Якщо на цьому етапі ви думаєте про те, як збираєтеся написати робочий код, ви робите це неправильно!

Саме на цій фазі ви зосереджуєтесь на написанні чистого інтерфейсу для майбутніх користувачів. На цьому етапі ви розробляєте, як ваш код буде використовуватися клієнтами.

Це перше правило є найважливішим, і саме воно відрізняє TDD від звичайного тестування. Ви пишете тест, щоб потім можна було написати виробничий код. Ви не пишете тест, щоб перевірити свій код.

Давайте розглянемо приклад.

// LeapYear.spec.jsdescribe('Leap year calculator', () => { it('should consider 1996 as leap', () => { expect(LeapYear.isLeap(1996)).toBe(true); });});

Наведений вище приклад є прикладом того, як тест може виглядати в JavaScript, використовуючи структуру тестування Jasmine. Вам не потрібно знати Жасмин - досить зрозуміти, що it(...)це тест і expect(...).toBe(...)спосіб змусити Жасмін перевірити, чи щось відповідає очікуваним.

У наведеному вище тесті я перевірив, що функція LeapYear.isLeap(...)повертається trueза 1996 рік. Ви можете подумати, що 1996 рік - це магічне число і, отже, погана практика. Це не так. У тестовому коді магічні цифри хороші, тоді як у виробничому коді їх слід уникати.

Цей тест насправді має деякі наслідки:

  • Назва калькулятора високосного року - LeapYear
  • isLeap(...)є статичним методом LeapYear
  • isLeap(...)приймає число (а не масив, наприклад) як аргумент і повертає trueабо false.

Це один тест, але насправді він має багато наслідків! Чи потрібен нам метод, щоб визначити, чи рік є високосним, чи потрібен метод, який повертає список високосних років між початковою та кінцевою датами? Чи мають значення елементи елементів? Ось такі запитання ви повинні мати на увазі під час написання тестів у червоній фазі.

На цьому етапі вам потрібно приймати рішення про те, як буде використовуватися код. Ви базуєтесь на тому, що вам справді потрібно на даний момент, а не на тому, що, на вашу думку, може знадобитися.

Ось ще одна помилка: не пишіть купу функцій / класів, які, на вашу думку, можуть вам знадобитися. Зосередьтеся на функції, яку ви впроваджуєте, і на тому, що насправді потрібно. Написання чогось, для чого не потрібна функція, є надмірною розробкою.

А як щодо абстракції? Побачимо це пізніше, на фазі рефактора.

Зелена фаза

Зазвичай це найпростіший етап, оскільки на цьому етапі ви пишете (виробничий) код. Якщо ви програміст, ви робите це постійно.

Тут виникає ще одна велика помилка: замість того, щоб написати достатньо коду, щоб пройти червоний тест, ви пишете всі алгоритми. Роблячи це, ви, мабуть, замислюєтесь над тим, яка реалізація є найбільш ефективною. У жодному разі!

На цьому етапі вам потрібно діяти як програміст, який має одне просте завдання: написати прямолінійне рішення, яке дозволяє пройти тест (а тривожний червоний на звіті про тестування стає зеленим). На цьому етапі вам дозволяється порушувати найкращі практики і навіть дублювати код. Дублювання коду буде видалено на етапі рефактора.

Але чому ми маємо це правило? Чому я не можу написати весь код, який вже є в моїй свідомості? З двох причин:

  • Просте завдання менш схильне до помилок, і ви хочете мінімізувати помилки.
  • Ви точно не хочете змішувати код, який перевіряється, з кодом, який не є. Ви можете написати код, який не тестується (він же застарілий), але найгірше, що ви можете зробити, це змішати перевірений і неперевірений код.

А як щодо чистого коду? А як щодо продуктивності? Що робити, якщо написання коду змушує мене виявити проблему? А як щодо сумнівів?

Продуктивність - довга історія, і вона виходить за рамки цієї статті. Скажімо просто, що налаштування продуктивності на цьому етапі є більшістю часу передчасною оптимізацією.

Тестова техніка розробки передбачає ще дві речі: список справ та фазу рефактора.

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!