0

TDD - Разработка через тестирование

 2 years ago
source link: https://dckms.github.io/system-architecture/emacsway/it/tdd/tdd.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Что такое TDD?

Прежде всего, что такое TDD (Test-Driven Development)?

Чистый код, который работает (clean code that works), — в этой короткой, но содержательной фразе, придуманной Роном Джеффризом (Ron Jeffries), кроется весь смысл методики Test-Driven Development (TDD). Чистый код, который работает, — это цель, к которой стоит стремиться, и этому есть причины.

Clean code that works - now. This is the seeming contradiction that lies behind much of the pain of programming. Test-driven development replies to this contradiction with a paradox-test the program before you write it.

—"Test-Driven Development By Example" 1 by Kent Beck, перевод П. Анджан

TDD - это о Software Design

Классическое заблуждение заключается в том, что TDD - это методика тестирования. На самом же деле, TDD - это, прежде всего, методика разработки и проектирования:

Ирония TDD состоит в том, что это вовсе не методика тестирования. Это методика анализа, методика проектирования, фактически методика структурирования всей деятельности, связанной с разработкой программного кода.

One of the ironies of TDD is that it isn't a testing technique (the Cunningham Koan). It's an analysis technique, a design technique, really a technique for structuring all the activities of development.

—"Test-Driven Development By Example" 1 by Kent Beck, перевод П. Анджан

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

It lets you write code with far fewer defects and a much cleaner design than is common in the industry. However, those whose souls are healed by the balm of elegance can find in TDD a way to do well by doing good.

—"Test-Driven Development By Example" 1 by Kent Beck, перевод П. Анджан

TDD базируется на очаровательно-наивном предположении программиста о том, что чем красивее код, тем вероятнее успех. TDD помогает вам обращать внимание на правильные вопросы в подходящие для этого моменты времени. Благодаря этому вы можете делать дизайн чище и модифицировать его по мере того, как перед вами встают новые обстоятельства.

TDD rests on a charmingly naive geekoid assumption that if you write better code, you'll be more successful. TDD helps you to pay attention to the right issues at the right time so you can make your designs cleaner, you can refine your designs as you learn.

—"Test-Driven Development By Example" 1 by Kent Beck, перевод П. Анджан

Мы с Робертом Мартином (Robert Martin) занимались исследованием подобного стиля TDD. Проблема состоит в том, что дизайн продолжает вас удивлять. Идеи, которые на первый взгляд кажутся вам вполне уместными, позже оказываются неправильными. Поэтому я не рекомендую целиком и полностью доверять своим предчувствиям относительно паттернов. Лучше думайте о том, что, по-вашему, должна делать система, позвольте дизайну оформиться так, как это необходимо.

Robert Martin and I did some research into this style of TDD. The problem is that the design keeps surprising you. Perfectly sensible design ideas turn out to be wrong. Better just to think about what you want the system to do, and let the design sort itself out later.

—"Test-Driven Development By Example" 1 by Kent Beck, перевод П. Анджан

TDD - это способ управления сложностью

Согласно закономерности Магического числа семь плюс-минус два, обнаруженной американским учёным-психологом Джорджем Миллером, кратковременная человеческая память, как правило, не может запомнить и повторить более 7 ± 2 элементов. Превышение этого порога замедляет темпы разработки, так как мозг не может преодолеть высокую концентрацию сложности.

Тут можно провести аналогию с народной пословицей: веник сложно поломать пока он связан, но, развязав его на отдельные прутики, их можно легко переломать по отдельности. TDD именно именно это и делает - декомпозирует сложность в процессе разработки.

Здесь хорошо прослеживается аналогия с рефакторингом, который, в значительной мере, был основан тем же самым человеком - Кент Беком.

Мой первый опыт проведения дисциплинированного "поэтапного" рефакторинга связан с программированием на пару с Кентом Беком (Kent Beck) на высоте 30 000 футов.

My first experience with disciplined, "one step at a time" refactoring was when I was pair-programming at 30,000 feet with Kent Beck.

—Martin Fowler, the key author of "Refactoring: Improving the Design of Existing Code" 5 by Martin Fowler, Kent Beck, John Brant, William Opdyke, Don Roberts, перевод С. Маккавеева

К тому же, рефакторинг является необъемлемой частью цикла TDD:

Красный—зеленый—рефакторинг — это мантра TDD.

Red/green/refactor - the TDD mantra.

—"Test-Driven Development By Example" 1 by Kent Beck, перевод П. Анджан

По основной версии, слово "refactoring" происходит от математического термина "factoring", и дословно переводится как "факторизация" или "декомпозиция", о чем говорит на своем сайте ключевой автор известной книги "Refactoring: Improving the Design of Existing Code" 5 (благодаря которой, рефакторинг, собственно, и стал популярным):

The obvious answer comes from the notion of factoring in mathematics. You can take an expressions such as x^2 + 5x + 6 and factor it into (x+2)(x+3). By factoring it you can make a number of mathematical operations much easier. Obviously this is much the same as representing 18 as 2*3^2. I've certainly often heard of people talking about a program as well factored once it's broken out into similarly logical chunks.

—"Etymology Of Refactoring" by Martin Fowler

Такое же мнение можно увидеть и на сайте Ward Cunningham:

Refactoring is a kind of reorganization. Technically, it comes from mathematics when you factor an expression into an equivalence - the factors are cleaner ways of expressing the same statement. Refactoring implies equivalence; the beginning and end products must be functionally identical. You can view refactoring as a special case of reworking (see WhatIsReworking).

Practically, refactoring means making code clearer and cleaner and simpler and elegant. Or, in other words, clean up after yourself when you code. Examples would run the range from renaming a variable to introducing a method into a third-party class that you don't have source for.

Refactoring is not rewriting, although many people think they are the same. There are many good reasons to distinguish them, such as regression test requirements and knowledge of system functionality. The technical difference between the two is that refactoring, as stated above, doesn't change the functionality (or information content) of the system whereas rewriting does. Rewriting is reworking. See WhatIsReworking.

Refactoring is a good thing because complex expressions are typically built from simpler, more grokable components. Refactoring either exposes those simpler components or reduces them to the more efficient complex expression (depending on which way you are going).

For an example of efficiency, count the terms and operators: (x - 1) * (x + 1) = x^2 - 1. Four terms versus three. Three operators versus two. However, the left hand side expression is (arguably) simpler to understand because it uses simpler operations. Also, it provides you more information about the structure of the function f(x) = x^2 - 1, like the roots are +/- 1, that would be difficult to determine just by "looking" at the right hand side.

—"What Is Refactoring" on wiki.c2.com

Если кому-то имя Ward Cunningham ни о чем не говорит, то вот как представил его сам Kent Beck в книге "Test-Driven Development By Example" 1:

Я начал свою жизнь настоящего программиста благодаря наставничеству и в рамках постоянного сотрудничества с Уордом Каннингэмом (Ward Cunningham). Иногда я рассматриваю разработку, основанную на тестах, как попытку предоставить каждому программисту, работающему в произвольной среде, ощущение комфорта и тесной дружбы, которое было у нас с Уордом, когда мы вместе разрабатывали программы Smalltalk в среде Smalltalk. He существует способа определить первоначальный источник идей, если два человека обладают одним общим мозгом. Если вы предположите, что все хорошие идеи на самом деле изначально придумал Уорд, вы не будете далеки от истины.

My life as a real programmer started with patient mentoring from and continuing collaboration with Ward Cunningham. Sometimes I see Test-Driven Development (TDD) as an attempt to give any software engineer, working in any environment, the sense of comfort and intimacy we had with our Smalltalk environment and our Smalltalk programs. There is no way to sort out the source of ideas once two people have shared a brain. If you assume that all of the good ideas here are Ward's, then you won't be far wrong.

—"Test-Driven Development By Example" 1 by Kent Beck, перевод П. Анджан

Ну и Википедия о факторизации:

Factorization (or factoring) may also refer to more general decompositions of a mathematical object into the product of smaller or simpler objects. For example, every function may be factored into the composition of a surjective function with an injective function.

—"Factorization", Wikipedia

Decomposition in computer science, also known as factoring, is breaking a complex problem or system into parts that are easier to conceive, understand, program, and maintain.

—"Decomposition", Wikipedia

В математике факториза́ция или фа́кторинг — это декомпозиция объекта (например, числа, полинома или матрицы) в произведение других объектов или факторов, которые, будучи перемноженными, дают исходный объект. Например, число 15 факторизуется на простые числа 3 и 5, а полином x2 − 4 факторизуется на (x − 2)(x + 2). В результате факторизации во всех случаях получается произведение более простых объектов, чем исходный.

—"Факторизация", Wikipedia

Таким образом, рефакторинг - это способ управления сложностью программы, который делает программу более читаемой и понимаемой за счет декомпозиции сложности, что позволяет снизить нагрузку на человеческую память. Процесс рефакторинга подобен факторизации математического выражения, в результате которого выводится более простое эквивалентное выражение, т.е. сохраняется функциональная идентичность. Именно поэтому рефакторинг оставляет неизменным внешнее поведение системы:

Рефакторинг представляет собой процесс такого изменения программной системы, при котором не меняется внешнее поведение кода, но улучшается его внутренняя структура.

Refactoring is the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure.

—Martin Fowler in "Refactoring: Improving the Design of Existing Code" 5 by Martin Fowler, Kent Beck, John Brant, William Opdyke, Don Roberts, перевод С. Маккавеева

TDD, как и рефакторинг, расщепляет сложность таким образом, чтобы минимизировать объем сложности, рассматриваемый разработчиком в единицу времени. Это как песочные часы - одна песчинка в единицу времени. Именно этим объясняется повышение темпов разработки при использовании TDD.

Каким образом можно модифицировать одну часть метода или объекта, состоящего из нескольких частей? Вначале изолируйте изменяемую часть. Мне приходит в голову аналогия с хирургической операцией: фактически все тело оперируемого пациента покрыто специальной простыней за исключением места, в котором, собственно, осуществляется операция. Благодаря такому покрытию хирург имеет дело с фиксированным набором переменных. Перед выполнением операции врачи сколь угодно долго могут обсуждать, какое влияние на здоровье пациента оказывает тот или иной орган, однако во время операции внимание хирурга должно быть сфокусировано.

How do you change one part of a multi-part method or object? First, isolate the part that has to change. The picture that comes to my mind is surgery: The entire patient except the part to be operated on is draped. The draping leaves the surgeon with only a fixed set of variables. Now, we could have long arguments over whether this abstraction of a person to a lower left quadrant abdomen leads to good health care, but at the moment of surgery, I'm kind of glad the surgeon can focus.

—"Test-Driven Development By Example" 1 by Kent Beck, перевод П. Анджан

В этом отношении, TDD можно сравнить с шорами.

Несмотря на множество появившихся в последнее время мощных инструментов, программирование по-прежнему остается сложной работой. Я часто ощущаю себя в ситуации, когда мне кажется, что я жонглирую шариками, и мне приходится следить за несколькими шариками в воздухе в одно и то же время: малейшая потеря внимания, и все сыпется на пол. Методика TDD позволяет избавиться от этого ощущения.

Когда вы работаете в стиле TDD, в воздухе постоянно находится лишь один шарик. Вы можете сконцентрироваться на нем, а значит, хорошо справиться со своей работой. Когда я добавляю в программу новую функциональность, я не думаю о том, какой дизайн должен быть реализован в данной функции. Я просто пытаюсь добиться срабатывания тестов самым простым из доступных мне способов. Когда я переключаюсь в режим рефакторинга, я не беспокоюсь о добавлении в программу новых функций, я думаю только о правильном дизайне. На каждом из этих этапов я концентрируюсь на единственной задаче, благодаря этому мое внимание не распыляется.

Despite all the fancy tools that we have, programming is still hard. I can remember many programming times when I feel like I was trying to keep several balls in the air at once, any lapse of concentration and everything would come tumbling down. Test-driven development helps reduce that feeling, and as a result you get this rapid unhurriedness.

I think the reason for this is that working in a test-driven development style gives you this sense of keeping just one ball in the air at once, so you can concentrate on that ball properly and do a really good job with it. When I'm trying to add some new functionality, I'm not worried about what really makes a good design for this piece of function, I'm just trying to get a test to pass as easily as I can. When I switch to refactoring mode, I'm not worried about adding some new function, I'm just worried about getting the right design. With both of these I'm just focused on one thing at a time, and as a result I can concentrate better on that one thing.

—Martin Fowler, Afterword, "Test-Driven Development By Example" 1, перевод П. Анджан

Снижение количества дефектов приводит к возникновению множества вторичных психологических и социальных эффектов. После того как я начал работать в стиле TDD, программирование стало для меня значительно менее нервным занятием. Когда я работаю в стиле TDD, мне не надо беспокоиться о множестве вещей. Вначале я могу заставить paботать только один тест, потом — все остальные. Уровень стресса существенно снизился. Взаимоотношения с партнерами по команде стали более позитивными. Разработанный мною код перестал быть причиной сбоев, люди стали в большей степени рассчитывать на него. У заказчиков тоже повысилось настроение. Теперь выпуск очередной версии системы означает новую функциональность, а не набор новых дефектов, которые добавляются к уже существующим.

Part of the effect certainly comes from reducing defects. The sooner you find and fix a defect, the cheaper it is, often dramatically so (just ask the Mars Lander). There are plenty of secondary psychological and social effects from reduced defects. My own practice of programming became much less stressful when I started with TDD. No longer did I have to worry about everything at once. I could make this test run, and then all the rest. Relationships with my teammates became more positive. I stopped breaking builds, andpeople could rely on my software to work. Customers of my systems became more positive, too. A new release of the system just meant more functionality, not a host of new defects to identify among all of their old favorite bugs.

—"Test-Driven Development By Example" 1 by Kent Beck, перевод П. Анджан

Иногда мозгу сложно удержать все в голове, и разработчик берется за листочек и ручку. При TDD, вместо листочка и ручки используется файловый редактор. TDD позволяет сфокусировать мозг на минимально возможной единице сложности, которую можно рассмотреть изолированно, что приводит к перераспределению умственных ресурсов. Кстати, именно это является одной из ключевых особенностей, благодаря которой, практикование TDD делает код чище.

Если рефакторинг помогает сосредоточиться на одной обязанности, выполняемой функцией, то TDD идет еще дальше, и помогает сосредоточиться на одном конкретном значении функции, а значит, - на одном из ее внутренних состояний. Это позволяет выводить алгоритм функции путем обобщения пересекаемых триангуляцией ее внутренних состояний (и поведений, производящих эти состояния). А это, в свою очередь, позволяет моделировать поведение функции небольшими законченными фрагментами, удовлетворяющими конкретным значениям функции, и визуализировать формирование поведения функции прямо в редакторе. Наглядно это демонстрируется на примере выведения функции Фибоначи в приложении книги, см. Appendix II. Fibonacci 1.

Это еще один паттерн рефакторинга: разработать код, который работает с некоторым конкретным экземпляром, и обобщить этот код так, чтобы он мог работать со всеми остальными экземплярами, для этого константы заменяются переменными. В данном случае роль константы играет не некоторое значение, а жестко фиксированный код (имя конкретного метода). Однако принцип остается одним и тем же. В рамках TDD эта проблема решается очень легко: методика TDD снабжает вас конкретными работающими примерами, исходя из которых вы можете выполнить обобщение. Это значительно проще, чем выполнять обобщение исходя только из собственных умозаключений.

Here is another general pattern of refactoring: take code that works in one instance and generalize it to work in many by replacing constants with variables. Here the constant was hardwired code, not a data value, but the principle is the same. TDD makes this work well by giving you running concrete examples from which to generalize, instead of having to generalize purely with reasoning.

—"Test-Driven Development By Example" 1 by Kent Beck, перевод П. Анджан

Контроль над объемом работы. Программисты привыкли пытаться предвидеть возникновение в будущем самых разнообразных проблем. Если вы начинаете с конкретного примера и затем осуществляете обобщение кода, это помогает вам избавиться от излишних опасений. Вы можете сконцентрироваться на решении конкретной проблемы и поэтому выполнить работу лучше. При переходе к следующему тесту вы опять же концентрируетесь на нем, так как знаете, что предыдущий тест гарантированно работает.

Scope control - Programmers are good at imagining all sorts of future problems. Starting with one concrete example and generalizing from there prevents you from prematurely confusing yourself with extraneous concerns. You can do a better job of solving the immediate problem because you are focused. When you go to implement the next test case, you can focus on that one, too, knowing that the previous test is guaranteed to work.

—"Test-Driven Development By Example" 1 by Kent Beck, перевод П. Анджан

Математическое объяснение этого явления можно найти в главе "1. Recurrent Problems : 1.1. The Tower of Hanoi" книги "Concrete Mathematics: A Foundation for Computer Science" 2nd edition by Ronald L. Graham, Donald E. Knuth, Oren Patashnik.

Кроме того, при TDD хорошо отслеживается ниточка, за которую можно распутать клубок сложности, и вопрос "с какого конца подступиться" решается сам собой.

Почему TDD быстрее

При TDD разработка осуществляется быстрее, хотя объема кода пишется больше. Суть в том, что в процессе конструирования кода, 91% времени занимает чтение кода и борьба со сложностью, и только 9% времени (1:10) занимает ввод символов с клавиатуры.

TDD является эффективным средством управления сложностью и снижения когнитивной нагрузки. А поскольку чтение кода и борьба со сложностью (обдумывание) занимает более 91% времени конструирования кода, то время на написание тестов полностью перекрывается повышением темпов разработки, т.е. разработка с тестами получается даже быстрей. Пальцы работают больше, а голова меньше. Происходит перераспределение составляющих разработки.

Допустим, что разработчику нужно написать вдвое больше кода без роста когнитивной нагрузки (написание тестов не требует борьбы со сложностью). Т.е. вместо соотношения 1:10 (где 1 - это часть времени ввода символов с клавиатуры, а 10 - это часть времени чтения кода и борьбы со сложностью) получится соотношение 2:10, что равно 17%:83% вместо 9%:91%. Совокупное время увеличится на 100%*(12 - 11)/11 = 9% - ровно столько времени потребуется свеху для того, чтобы написать вдвое больше кода без роста когнитивной нагрузки.

А теперь представим, что удалось снизить когнитивную нагрузку вдвое. Т.е. вместо соотношения 1:10 (где 1 - это часть времени ввода символов с клавиатуры, а 10 - это часть времени чтения кода и борьбы со сложностью) получится соотношение 1:5, что равно 17%:83% вместо 9%:91%. Совокупное время уменьшится на 100%*(6 - 11)/11 = -45% - ровно столько времени сэкономится, если разработчик будет тратить вдвое меньше времени на борьбу со сложностью.

9% (вдвое больше кода) против 45% (вдвое меньше думать).

Конечно, коэффициенты в этом примере сильно завышены, но они хорошо раскрывают механизм ускорения темпов разработки с использованием TDD. На практике TDD дает прирост разработки около 10% - Jason Gorman публиковал свою статистику многократного прохождения кат как по TDD, так и без TDD (см. главу "Chapter 1. What Is Design and Architecture? :: What went wrong?" книги "Clean Architecture: A Craftsman's Guide to Software Structure and Design" 4 by Robert C. Martin).

Я перепроверял эту особенность на личном опыте, и убедился в том, что это, действительно, работает.

Кроме того, время на написание тестов можно прогнозировать, в отличии от отладки.

TDD - основной катализатор Clean Code

Каким образом тестирование улучшает качество кода?

"The problem with testing code is that you have to isolate that code. It is often difficult to test a function if that function calls other functions. To write that test you've got to figure out some way to decouple the function from all the others. In other words, the need to test first forces you to think about good design.

If you don't write your tests first, there is no force preventing you from coupling the functions together into an untestable mass. If you write your tests later, you may be able to test the inputs and the outputs of the total mass, but it will probably be quite difficult to test the individual functions."

—"Clean Coder" 2 by Robert Martin

Однако, нужно учитывать:

Я сказал, что предположение наивное, однако, скорее всего, я преувеличил. На самом деле наивно предполагать, что чистый код — это все, что необходимо для успеха. Мне кажется, что хорошее проектирование — это лишь 20% успеха. Безусловно, если проектирование будет плохим, вы можете быть на 100% уверены в том, что проект провалится. Однако приемлемый дизайн сможет обеспечить успех проекта только в случае, если остальные 80% будут там, где им полагается быть.

I say "naive," but that's perhaps overstating. What's naive is assuming that clean code is all there is to success. Good engineering is maybe 20 percent of a project's success. Bad engineering will certainly sink projects, but modest engineering can enable project success as long as the other 80 percent lines up right.

—"Test-Driven Development By Example" 1 by Kent Beck, перевод П. Анджан


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK