Тестово задвижване: какво е и какво не.

Разработеното с тестове стана популярно през последните няколко години. Много програмисти са изпробвали тази техника, не са успели и са стигнали до извода, че TDD не си струва усилията, които изисква.

Някои програмисти смятат, че на теория това е добра практика, но че никога няма достатъчно време да се използва наистина TDD. А други смятат, че всъщност това е загуба на време.

Ако се чувствате по този начин, мисля, че може да не разберете какво всъщност е TDD. (Добре, предишното изречение беше да привлече вниманието ви). Има много добра книга за TDD, Test Driven Development: By Example, от Кент Бек, ако искате да я проверите и да научите повече.

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

Защо да използвам TDD?

Има проучвания, статии и дискусии за това колко е ефективен TDD. Въпреки че определено е полезно да имаме някои числа, не мисля, че те отговарят на въпроса защо първо трябва да използваме TDD.

Кажете, че сте уеб разработчик. Току-що завършихте малка функция. Смятате ли, че е достатъчно да тествате тази функция само чрез ръчно взаимодействие с браузъра? Не мисля, че е достатъчно да разчитаме само на тестове, направени от разработчиците ръчно. За съжаление това означава, че част от кода не е достатъчно добра.

Но разглеждането по-горе е за тестване, а не за самия TDD. Така че защо TDD? Краткият отговор е „защото това е най-простият начин за постигане както на добро качество на кода, така и на добро тестово покритие“.

По-дългият отговор идва от това, което всъщност е TDD ... Нека започнем с правилата.

Правила на играта

Чичо Боб описва TDD с три правила:

- Нямате право да пишете произволен код, освен ако не е необходимо да направите неуспешно преминаване на единичен тест. - Нямате право да пишете повече от единичен тест, отколкото е достатъчно за неуспех; и неуспехите при компилацията са неуспехи. - Не ви е позволено да пишете повече производствен код, отколкото е достатъчно, за да преминете един неуспешен единичен тест.

Харесва ми и по-кратка версия, която намерих тук:

- Напишете само достатъчно единичен тест, за да се провали. - Напишете само достатъчно производствен код, за да премине неуспешното единично изпитване.

Тези правила са прости, но хората, които се обръщат към TDD, често нарушават едно или повече от тях. Предизвиквам ви: можете ли да напишете малък проект, спазвайки стриктно тези правила? Под малък проект имам предвид нещо реално, а не само пример, който изисква около 50 реда код.

Тези правила определят механиката на TDD, но определено не са всичко, което трябва да знаете. Всъщност процесът на използване на TDD често се описва като цикъл Red / Green / Refactor. Да видим за какво става въпрос.

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

Червена фаза

В червената фаза трябва да напишете тест за поведение, което предстои да приложите. Да, написах поведение . Думата „тест“ в Test Driven Development е подвеждаща. На първо място трябваше да го наречем „Развитие на поведението“. Да, знам, някои хора твърдят, че 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.

Това е един тест, но всъщност има много последици! Имаме ли нужда от метод, който да определи дали една година е високосна, или се нуждаем от метод, който връща списък на високосните години между начална и крайна дата? Смислено ли е името на елементите? Това са видовете въпроси, които трябва да имате предвид, докато пишете тестове в червената фаза.

В тази фаза трябва да вземете решения за това как ще се използва кода. Базирате това на това, което наистина ви е необходимо в момента, а не на това, което смятате, че може да е необходимо.

Тук идва друга грешка: не пишете куп функции / класове, които смятате, че може да ви потрябват. Концентрирайте се върху функцията, която прилагате, и върху това, което наистина е необходимо. Писането на нещо, което функцията не изисква, е свръх инженеринг.

Ами абстракцията? Ще видим това по-късно, във фазата на рефактор.

Зелена фаза

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!