Что я ещё хочу сказать о TDD
Предыдущая серия была тут - http://programmingmindstream.blogspot.ru/2013/11/tdd.html
Многие люди с которыми я общался насчёт тестов вообще и TDD в частности "разбивались" о следующее препятствие:
TestFirst. TestDriven.
Я не хочу "спорить", а тем более опровергать или критиковать никого из уважаемых "теоретиков TDD".
Я лишь хочу описать свою трактовку. Ну или - как я это "для себя" понял.
TDD говорит нам примерно следующее:
1. Запустите ВСЕ тесты, убедитесь, что они прошли.
2. Добавьте новый тест для новой функциональности. Убедитесь, что он не прошёл.
3. Добавьте функциональность. Убедитесь, что тест прошёл.
Какой вопрос сразу возникает у людей?
А вот какой: "Как я могу написать тест к тому, чего НЕТ?"?
И люди - ПРАВЫ.
Я сам когда впервые смотрел на TTD - думал - "вот чушь собачья". (Это - "эпитет", а не "ругательство")
"Как можно писать тест к тому чего НЕТ? Пусть даже и НЕ проходящий.
"Пойди туда не знаю куда..."
Я долго думал и вот, что я для себя надумал:
НЕТУ TestFirst.
НЕТУ TestFirst.
В ПЕРВУЮ очередь - есть "набросок" АРХИТЕКТУРЫ. Не код, ни тесты, ни что-то ещё. А именно - "набросок" АРХИТЕКТУРЫ.
И TestFirst делается не для чего-то "абстрактного", что "будет когда-то потом". А для ВПОЛНЕ КОНКРЕТНОГО.
Описанного в архитектуре и растущими ногами из ТЗ.
Простейший пример:
Пусть нам надо сделать "кнопку", которая зовёт новую функциональность.
Тогда тест может быть такой:
И он КОНЕЧНО не пройдёт - пока мы не добавим кнопку и пройдёт - КОГДА мы её таки ДОБАВИМ.
Но! Тест "ЗНАЕТ" о существовании кнопки. Он знает хотя бы её имя. Т.е. он тестирует не "непонятно что", а вполне "понятно что". Часть концепций архитектуры. КНОПКА, описанная в ТЗ и архитектуре.
Идём дальше.
Например нам надо написать класс TmyIntegerList (я его разберу подробнее ниже).
И что мы делаем?
Мы пишем:
"Дизайн архитектуры" и заготовку проектного класса.
ИМЕННО их, а не ТЕСТ.
Ещё раз:
Мы пишем: "Дизайн архитектуры" и заготовку проектного класса.
ИМЕННО их, а не ТЕСТ.
Как-то так:
А ПОТОМ только, мы пишем ТЕСТ.
Ещё РАЗ - ТОЛЬКО потом пишем ТЕСТ.
Примерно такой:
И этот тест - КОНЕЧНО же - НЕ ПРОЙДЁТ. А вот ПОТОМ, когда мы добавим РЕАЛИЗАЦИЮ метода TmyIntegerList.Add - он пройдёт.
Итак.
Нету никакого TestFirst.
Если уж говорить, про "first" - то ПЕРВИЧНЫ архитектура и дизайн, а лишь потом - ТЕСТЫ. А лишь потом - реализация.
Итак цепочка разработки выглядит так:
ТЗ -> Набросок архитектуры -> Тест -> Код
А дальше возможны все разные варианты:
ТЗ -> Набросок архитектуры -> Тест -> Код -> Тест -> Код
ТЗ -> Набросок архитектуры -> Тест -> Код -> Архитектура -> Тест -> Код
ТЗ -> Набросок архитектуры -> Тест -> Код -> Архитектура -> Тест -> Код -> ТЗ -> Архитектура -> Тест -> Код
и т.д. и т.п.
First - это - ТЗ и набросок архитектуры - дальше начинается - "итерационная разработка".
И что - особенно ВКУСНО - это то, что как только мы написали тест - нам НЕ НАДО думать - "а где проверять работоспособность нашего класса".
Мы УЖЕ настроили всю инфраструктуру для его тестирования и проверки.
ИМЕННО в этом и заключается слово Driven в TDD.
В том, что ТЕСТЫ - ПОМОГАЮТ, а не МЕШАЮТ процессу разработки.
Тесты - "ведут" за собой разработчика.
Тесты влияют на код, а код на тесты.
А они в свою очередь ВСЕ ВМЕСТЕ влияют на архитектуру и ТЗ.
А главное, что раз тесты "ведут" за собой, то они всё же так или иначе - написаны и проверяют работоспособность проектных классов. А значит - нет НИ МАЛЕЙШЕГО резона их не использовать.
И даже если вы напишете что-то вроде:
И "типа" не использовали класс TmyIntegerList, то это не значит, что класса TmyIntegerList - "не существует в природе". Он есть как минимум "у вас в голове". Раз вы написали что-то вроде TmyIntegerListTests.ListAdd, то это - ОЗНАЧАЕТ, что есть нечто, на чём есть операция Add.
Если вы скажете мне "фуу.. список целых чисел... как банально... протестируйте нам ядерный реактор..."
Отвечу - "да банально". Но! "Дорога из тысячи ли начинается с одного шага". И "ядерные реакторы" где-то внутри "состоят из списков целых".
А если захотите РЕАЛЬНЫЕ примеры тестов - так их "есть у меня". ЦЕЛЫЙ вагон. Готов показать, "в обмен" на ваш "хотя бы один тест". Чтобы "РАЗГОВАРИВАТЬ НА РАВНЫХ". А не лить воду на неработаущую мельницу.
Теперь ремарка о том откуда по моему скромному пониманию взялось понятие TestFirst и "протестируй сначала то, чего - нету".
Мне кажется, что тут всё дело, что всё ногами уходит в Java и JUnit, где очень сильно используется рефлексия, duck-typing, инъекции и прочие подобные "кунштюки".
И там можно написать "примерно" такой тест:
Я совсем не большой знаток Java, но надеюсь, что идея - понятна.
И тут получается такой момент - "хвост виляет собакой". Мы "вроде бы" НИЧЕГО не знаем про тестируемый класс, но однако можем "протестировать" его не написав его сначала. TestFirst. Типа...
Но давайте подумаем - "а действительно ли мы не знаем про тестируемый класс"? НЕТ КОНЕЧНО. Мы знаем его имя и знаем, что у него есть метод Add.
TestFirst? Да - хрена лысого! ArchitectureFirst!!!
Пусть мы даже и не написали НИ ОДНОЙ строчки нашего класса - мы УЖЕ начали ПРОЕКТИРОВАТЬ его АРХИТЕКТУРУ, а ТОЛЬКО потом - НАЧАЛИ писать тест.
Пусть даже не написано НИ ОДНОЙ строчки кода, кроме теста, но! "Дизайн архитектуры" - ЕСТЬ и при подходе с рефлексией и duck-typing'ом. Он ЕСТЬ - ХОТЯ БЫ "у нас в голове".
Но он - ЕСТЬ!
ArchitectureFirst!!!
Надеюсь, что моя мысль - понятна и развеявает сомнения насчёт "Как я могу написать тест к тому, чего НЕТ?"?
Не то "чего нет"! А то "что находится в процессе разработки и дизайна". Надеюсь это - понятно.
И это - СОВСЕМ не значит, что я "критикую" TDD, СОВСЕМ наоборот - я пытаюсь сделать всё возможное, чтобы как можно больше программистов прониклись этой идеей.
И пытаюсь "развеять сомнения" и показать, что "это совсем несложно".
Теперь позволю себе привести пример - "как я обычно делаю".
В примере я буду придерживаться САМОГО плохого и пессимистичного сценария - когда ни заказчики, ни "Группа Качества" не реагируют на вопросы разработчика.
И разработчику приходится либо "придумывать от себя", либо писать Assert'ы - http://18delphi.blogspot.ru/2013/04/blog-post.html
Весь код примера доступен в SVN тут - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/TDD/Chapter0/
Пусть нам надо написать всё тот же класс TmyIntegerList.
И пусть его спецификация такова:
TmyIntegerList - список целых чисел.
Поддерживает операции:
1. Вставки элемента.
2. Добавления элемента.
3. Удаления элемента.
4. Получения количества элементов.
5. Получения значения элемента по его порядковому номеру.
Это "типа" - ТЗ.
Напишем "рыбу" теста:
И тест - конечно же не пройдёт.
Теперь зафиксируем "набросок архитектуры" в проектном классе.
А именно - перенесём то, что "сидело в нашем мозгу" когда мы писали первый тест, на "бумагу".
Опишем "прототип" нашего класса:
И видоизменим тесты:
-- теперь в тестах мы уже ЗНАЕМ про РЕАЛЬНЫЙ проектный класс и используем его.
Тесты в таком виде - БЕЗУСЛОВНО не пройдут.
Пойдём дальше.
Реализуем хотя бы один из методов нашего проектного класса.
А лучше - ДВА. Самых простых - Add и Count.
Вот как это выглядит:
-- увидим, что два теста - ПРОШЛИ. Не ФАКТ, что ПРАВИЛЬНО, но ПРОШЛИ.
Вот тут мы сталкиваемся именно с тем, что называется - Driven.
Наполнили "скелет" прототипа "мясом" и сразу получили отклик от тестов.
Пойдём дальше.
Реализуем остальные методы нашего проектного класса:
Запускаем тесты и что мы видим?
ОДИН тест - НЕ прошёл - TmyIntegerListTests.ListItem - там случилось AV.
А причина в методе TmyIntegerList.pm_GetItem.
Что делать?
Давайте перепишем метод TmyIntegerList.pm_GetItem так:
Запускаем тесты и они - ПРОШЛИ!
Сделали ли мы свою работу? "Наверное". Но не факт.
Давайте предположим, что мы отправили нашу разработку в "Группу Качества" и она нашла следующее:
И "открыла тикет в QC с номером 1".
После "выяснения подробностей" и "переговоров с ГК" - УБЕЖДАЕМСЯ, что ошибка - таки есть.
Пишем НОВЫЙ ТЕСТ:
И убеждаемся, что он таки НЕ проходит!
Что делать?
Правим код проектного класса:
Запускаем тесты - они - проходят. Сделали ли мы свою работу? НЕ ЗНАЮ. Наверное...
Теперь давайте предположим, что ГК нашла ещё одну ошибку:
... to be continued ...
Продолжать? Или "идея и так понятна"?
Предыдущая серия была тут - http://programmingmindstream.blogspot.ru/2013/11/tdd.html
Многие люди с которыми я общался насчёт тестов вообще и TDD в частности "разбивались" о следующее препятствие:
TestFirst. TestDriven.
Я не хочу "спорить", а тем более опровергать или критиковать никого из уважаемых "теоретиков TDD".
Я лишь хочу описать свою трактовку. Ну или - как я это "для себя" понял.
TDD говорит нам примерно следующее:
1. Запустите ВСЕ тесты, убедитесь, что они прошли.
2. Добавьте новый тест для новой функциональности. Убедитесь, что он не прошёл.
3. Добавьте функциональность. Убедитесь, что тест прошёл.
Какой вопрос сразу возникает у людей?
А вот какой: "Как я могу написать тест к тому, чего НЕТ?"?
И люди - ПРАВЫ.
Я сам когда впервые смотрел на TTD - думал - "вот чушь собачья". (Это - "эпитет", а не "ругательство")
"Как можно писать тест к тому чего НЕТ? Пусть даже и НЕ проходящий.
"Пойди туда не знаю куда..."
Я долго думал и вот, что я для себя надумал:
НЕТУ TestFirst.
НЕТУ TestFirst.
В ПЕРВУЮ очередь - есть "набросок" АРХИТЕКТУРЫ. Не код, ни тесты, ни что-то ещё. А именно - "набросок" АРХИТЕКТУРЫ.
И TestFirst делается не для чего-то "абстрактного", что "будет когда-то потом". А для ВПОЛНЕ КОНКРЕТНОГО.
Описанного в архитектуре и растущими ногами из ТЗ.
Простейший пример:
Пусть нам надо сделать "кнопку", которая зовёт новую функциональность.
Тогда тест может быть такой:
AssureThatButtonExists('SomeButton');
И он КОНЕЧНО не пройдёт - пока мы не добавим кнопку и пройдёт - КОГДА мы её таки ДОБАВИМ.
Но! Тест "ЗНАЕТ" о существовании кнопки. Он знает хотя бы её имя. Т.е. он тестирует не "непонятно что", а вполне "понятно что". Часть концепций архитектуры. КНОПКА, описанная в ТЗ и архитектуре.
Идём дальше.
Например нам надо написать класс TmyIntegerList (я его разберу подробнее ниже).
И что мы делаем?
Мы пишем:
"Дизайн архитектуры" и заготовку проектного класса.
ИМЕННО их, а не ТЕСТ.
Ещё раз:
Мы пишем: "Дизайн архитектуры" и заготовку проектного класса.
ИМЕННО их, а не ТЕСТ.
Как-то так:
type TmyIntegerList = class public procedure Add(anItem: Integer); end;//TmyIntegerList procedure TmyIntegerList.Add(anItem: Integer); begin Assert(false, 'Не реализовано'); end;
А ПОТОМ только, мы пишем ТЕСТ.
Ещё РАЗ - ТОЛЬКО потом пишем ТЕСТ.
Примерно такой:
procedure TmyIntegerListTest.ListAdd; var l_List : TmyIntegerList; begin l_List := TmyIntegerList.Create; try l_List.Add(47879); finally FreeAndNil(l_List); end; end;
И этот тест - КОНЕЧНО же - НЕ ПРОЙДЁТ. А вот ПОТОМ, когда мы добавим РЕАЛИЗАЦИЮ метода TmyIntegerList.Add - он пройдёт.
Итак.
Нету никакого TestFirst.
Если уж говорить, про "first" - то ПЕРВИЧНЫ архитектура и дизайн, а лишь потом - ТЕСТЫ. А лишь потом - реализация.
Итак цепочка разработки выглядит так:
ТЗ -> Набросок архитектуры -> Тест -> Код
А дальше возможны все разные варианты:
ТЗ -> Набросок архитектуры -> Тест -> Код -> Тест -> Код
ТЗ -> Набросок архитектуры -> Тест -> Код -> Архитектура -> Тест -> Код
ТЗ -> Набросок архитектуры -> Тест -> Код -> Архитектура -> Тест -> Код -> ТЗ -> Архитектура -> Тест -> Код
и т.д. и т.п.
First - это - ТЗ и набросок архитектуры - дальше начинается - "итерационная разработка".
И что - особенно ВКУСНО - это то, что как только мы написали тест - нам НЕ НАДО думать - "а где проверять работоспособность нашего класса".
Мы УЖЕ настроили всю инфраструктуру для его тестирования и проверки.
ИМЕННО в этом и заключается слово Driven в TDD.
В том, что ТЕСТЫ - ПОМОГАЮТ, а не МЕШАЮТ процессу разработки.
Тесты - "ведут" за собой разработчика.
Тесты влияют на код, а код на тесты.
А они в свою очередь ВСЕ ВМЕСТЕ влияют на архитектуру и ТЗ.
А главное, что раз тесты "ведут" за собой, то они всё же так или иначе - написаны и проверяют работоспособность проектных классов. А значит - нет НИ МАЛЕЙШЕГО резона их не использовать.
И даже если вы напишете что-то вроде:
unit myListTests; interface uses TestFramework ; type TmyIntegerListTests = class(TTestCase) published procedure ListAdd; end;//TmyIntegerListTests implementation procedure TmyIntegerListTests.ListAdd; begin Assert(false, 'Не реализовано'); end; initialization TestFramework.RegisterTest(TmyIntegerListTests.Suite); end.
И "типа" не использовали класс TmyIntegerList, то это не значит, что класса TmyIntegerList - "не существует в природе". Он есть как минимум "у вас в голове". Раз вы написали что-то вроде TmyIntegerListTests.ListAdd, то это - ОЗНАЧАЕТ, что есть нечто, на чём есть операция Add.
Если вы скажете мне "фуу.. список целых чисел... как банально... протестируйте нам ядерный реактор..."
Отвечу - "да банально". Но! "Дорога из тысячи ли начинается с одного шага". И "ядерные реакторы" где-то внутри "состоят из списков целых".
А если захотите РЕАЛЬНЫЕ примеры тестов - так их "есть у меня". ЦЕЛЫЙ вагон. Готов показать, "в обмен" на ваш "хотя бы один тест". Чтобы "РАЗГОВАРИВАТЬ НА РАВНЫХ". А не лить воду на неработаущую мельницу.
Теперь ремарка о том откуда по моему скромному пониманию взялось понятие TestFirst и "протестируй сначала то, чего - нету".
Мне кажется, что тут всё дело, что всё ногами уходит в Java и JUnit, где очень сильно используется рефлексия, duck-typing, инъекции и прочие подобные "кунштюки".
И там можно написать "примерно" такой тест:
procedure TmyIntegerListTest.ListAdd; var l_List : Object; begin l_List := Framework.GetClassByName('TmyIntegerList').Create; try l_List.MethodByName.Execute('Add', [47879]); finally FreeAndNil(l_List); end; end;
Я совсем не большой знаток Java, но надеюсь, что идея - понятна.
И тут получается такой момент - "хвост виляет собакой". Мы "вроде бы" НИЧЕГО не знаем про тестируемый класс, но однако можем "протестировать" его не написав его сначала. TestFirst. Типа...
Но давайте подумаем - "а действительно ли мы не знаем про тестируемый класс"? НЕТ КОНЕЧНО. Мы знаем его имя и знаем, что у него есть метод Add.
TestFirst? Да - хрена лысого! ArchitectureFirst!!!
Пусть мы даже и не написали НИ ОДНОЙ строчки нашего класса - мы УЖЕ начали ПРОЕКТИРОВАТЬ его АРХИТЕКТУРУ, а ТОЛЬКО потом - НАЧАЛИ писать тест.
Пусть даже не написано НИ ОДНОЙ строчки кода, кроме теста, но! "Дизайн архитектуры" - ЕСТЬ и при подходе с рефлексией и duck-typing'ом. Он ЕСТЬ - ХОТЯ БЫ "у нас в голове".
Но он - ЕСТЬ!
ArchitectureFirst!!!
Надеюсь, что моя мысль - понятна и развеявает сомнения насчёт "Как я могу написать тест к тому, чего НЕТ?"?
Не то "чего нет"! А то "что находится в процессе разработки и дизайна". Надеюсь это - понятно.
И это - СОВСЕМ не значит, что я "критикую" TDD, СОВСЕМ наоборот - я пытаюсь сделать всё возможное, чтобы как можно больше программистов прониклись этой идеей.
И пытаюсь "развеять сомнения" и показать, что "это совсем несложно".
Теперь позволю себе привести пример - "как я обычно делаю".
В примере я буду придерживаться САМОГО плохого и пессимистичного сценария - когда ни заказчики, ни "Группа Качества" не реагируют на вопросы разработчика.
И разработчику приходится либо "придумывать от себя", либо писать Assert'ы - http://18delphi.blogspot.ru/2013/04/blog-post.html
Весь код примера доступен в SVN тут - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/TDD/Chapter0/
Пусть нам надо написать всё тот же класс TmyIntegerList.
И пусть его спецификация такова:
TmyIntegerList - список целых чисел.
Поддерживает операции:
1. Вставки элемента.
2. Добавления элемента.
3. Удаления элемента.
4. Получения количества элементов.
5. Получения значения элемента по его порядковому номеру.
Это "типа" - ТЗ.
Напишем "рыбу" теста:
program TDD; uses Vcl.Forms, GUITestRunner, myListTests in 'Tests\myListTests.pas'; {$R *.res} begin Application.Initialize; GUITestRunner.RunRegisteredTests; end. unit myListTests; interface uses TestFramework ; type TmyIntegerListTests = class(TTestCase) published procedure ListAdd; procedure ListInsert; procedure ListDelete; procedure ListCount; procedure ListItem; end;//TmyIntegerListTests implementation procedure TmyIntegerListTests.ListAdd; begin Assert(false, 'Не реализовано'); end; procedure TmyIntegerListTests.ListInsert; begin Assert(false, 'Не реализовано'); end; procedure TmyIntegerListTests.ListDelete; begin Assert(false, 'Не реализовано'); end; procedure TmyIntegerListTests.ListCount; begin Assert(false, 'Не реализовано'); end; procedure TmyIntegerListTests.ListItem; begin Assert(false, 'Не реализовано'); end; initialization TestFramework.RegisterTest(TmyIntegerListTests.Suite); end.-- в ней мы "типа не знаем" о том, ЧТО тестируем, но на самом деле - "набросок архитектуры" УЖЕ сидит "у нас в голове". Мы знаем про методы Add, Insert, Delete, Count и Item.
И тест - конечно же не пройдёт.
Теперь зафиксируем "набросок архитектуры" в проектном классе.
А именно - перенесём то, что "сидело в нашем мозгу" когда мы писали первый тест, на "бумагу".
Опишем "прототип" нашего класса:
unit myIntegerList; interface type TmyIntegerList = class public type IndexType = Integer; ItemType = Integer; protected function pm_GetCount: IndexType; function pm_GetItem(anIndex: IndexType): ItemType; public procedure Add(anItem: ItemType); procedure Insert(anIndex: IndexType; anItem: ItemType); procedure Delete(anIndex: IndexType); property Count: IndexType read pm_GetCount; property Items[anIndex: IndexType]: ItemType read pm_GetItem; end;//TmyIntegerList implementation // TmyIntegerList function TmyIntegerList.pm_GetCount: IndexType; begin Result := -1; Assert(false, 'Не реализовано'); end; function TmyIntegerList.pm_GetItem(anIndex: IndexType): ItemType; begin Result := -1; Assert(false, 'Не реализовано'); end; procedure TmyIntegerList.Add(anItem: ItemType); begin Assert(false, 'Не реализовано'); end; procedure TmyIntegerList.Insert(anIndex: IndexType; anItem: ItemType); begin Assert(false, 'Не реализовано'); end; procedure TmyIntegerList.Delete(anIndex: IndexType); begin Assert(false, 'Не реализовано'); end; end.
И видоизменим тесты:
unit myIntegerListTests; interface uses TestFramework ; type TmyIntegerListTests = class(TTestCase) published procedure ListAdd; procedure ListInsert; procedure ListDelete; procedure ListCount; procedure ListItem; end;//TmyIntegerListTests implementation uses System.SysUtils, myIntegerList ; procedure TmyIntegerListTests.ListAdd; var l_List : TmyIntegerList; begin l_List := TmyIntegerList.Create; try l_List.Add(Random(1000)); finally FreeAndNil(l_List); end;//try..finally end; procedure TmyIntegerListTests.ListInsert; var l_List : TmyIntegerList; begin l_List := TmyIntegerList.Create; try l_List.Insert(0, Random(1000)); finally FreeAndNil(l_List); end;//try..finally end; procedure TmyIntegerListTests.ListDelete; var l_List : TmyIntegerList; begin l_List := TmyIntegerList.Create; try l_List.Delete(0); finally FreeAndNil(l_List); end;//try..finally end; procedure TmyIntegerListTests.ListCount; var l_List : TmyIntegerList; begin l_List := TmyIntegerList.Create; try l_List.Count; finally FreeAndNil(l_List); end;//try..finally end; procedure TmyIntegerListTests.ListItem; var l_List : TmyIntegerList; begin l_List := TmyIntegerList.Create; try l_List.Item[0]; finally FreeAndNil(l_List); end;//try..finally end; initialization TestFramework.RegisterTest(TmyIntegerListTests.Suite); end.
-- теперь в тестах мы уже ЗНАЕМ про РЕАЛЬНЫЙ проектный класс и используем его.
Тесты в таком виде - БЕЗУСЛОВНО не пройдут.
Пойдём дальше.
Реализуем хотя бы один из методов нашего проектного класса.
А лучше - ДВА. Самых простых - Add и Count.
Вот как это выглядит:
unit myIntegerList; interface type TmyIntegerList = class public type IndexType = Integer; ItemType = Integer; private type ItemsArray = array of ItemType; private f_Items : ItemsArray; protected function pm_GetCount: IndexType; function pm_GetItem(anIndex: IndexType): ItemType; public procedure Add(anItem: ItemType); procedure Insert(anIndex: IndexType; anItem: ItemType); procedure Delete(anIndex: IndexType); property Count: IndexType read pm_GetCount; property Item[anIndex: IndexType]: ItemType read pm_GetItem; end;//TmyIntegerList implementation // TmyIntegerList function TmyIntegerList.pm_GetCount: IndexType; begin Result := Length(f_Items); end; function TmyIntegerList.pm_GetItem(anIndex: IndexType): ItemType; begin Result := -1; Assert(false, 'Не реализовано'); end; procedure TmyIntegerList.Add(anItem: ItemType); begin SetLength(f_Items, Length(f_Items) + 1); f_Items[High(f_Items)] := anItem; end; procedure TmyIntegerList.Insert(anIndex: IndexType; anItem: ItemType); begin Assert(false, 'Не реализовано'); end; procedure TmyIntegerList.Delete(anIndex: IndexType); begin Assert(false, 'Не реализовано'); end; end.
-- увидим, что два теста - ПРОШЛИ. Не ФАКТ, что ПРАВИЛЬНО, но ПРОШЛИ.
Вот тут мы сталкиваемся именно с тем, что называется - Driven.
Наполнили "скелет" прототипа "мясом" и сразу получили отклик от тестов.
Пойдём дальше.
Реализуем остальные методы нашего проектного класса:
unit myIntegerList; interface type TmyIntegerList = class public type IndexType = Integer; ItemType = Integer; private type ItemsArray = array of ItemType; private f_Items : ItemsArray; protected function pm_GetCount: IndexType; function pm_GetItem(anIndex: IndexType): ItemType; public procedure Add(anItem: ItemType); procedure Insert(anIndex: IndexType; anItem: ItemType); procedure Delete(anIndex: IndexType); property Count: IndexType read pm_GetCount; property Item[anIndex: IndexType]: ItemType read pm_GetItem; end;//TmyIntegerList implementation // TmyIntegerList function TmyIntegerList.pm_GetCount: IndexType; begin Result := Length(f_Items); end; function TmyIntegerList.pm_GetItem(anIndex: IndexType): ItemType; begin Result := f_Items[anIndex]; end; procedure TmyIntegerList.Add(anItem: ItemType); begin SetLength(f_Items, Length(f_Items) + 1); f_Items[High(f_Items)] := anItem; end; procedure TmyIntegerList.Insert(anIndex: IndexType; anItem: ItemType); begin if (anIndex = Self.Count) then Add(anItem) else Assert(false, 'Не реализовано'); // - ну не знаю я что тут делать, ТЗ типа - НЕПОЛНОЕ end; procedure TmyIntegerList.Delete(anIndex: IndexType); begin if (anIndex < 0) OR (anIndex >= Self.Count) then // - ну нечего тут удалять Exit else Assert(false, 'Не реализовано'); end; end.
Запускаем тесты и что мы видим?
ОДИН тест - НЕ прошёл - TmyIntegerListTests.ListItem - там случилось AV.
А причина в методе TmyIntegerList.pm_GetItem.
Что делать?
Давайте перепишем метод TmyIntegerList.pm_GetItem так:
function TmyIntegerList.pm_GetItem(anIndex: IndexType): ItemType; begin if (Self.Count = 0) then // - ну не знаю я тут, ЧТО делать - в ТЗ - НЕ оговорено Result := Random(5676) else Result := f_Items[anIndex]; end;
Запускаем тесты и они - ПРОШЛИ!
Сделали ли мы свою работу? "Наверное". Но не факт.
Давайте предположим, что мы отправили нашу разработку в "Группу Качества" и она нашла следующее:
var l_List : TmyIntegerList; begin l_List := TmyIntegerList.Create; try l_List.Add(Random(54365)); l_List.Delete(0); finally FreeAndNil(l_List); end;//try..finally end;
И "открыла тикет в QC с номером 1".
После "выяснения подробностей" и "переговоров с ГК" - УБЕЖДАЕМСЯ, что ошибка - таки есть.
Пишем НОВЫЙ ТЕСТ:
procedure TmyIntegerListTests.QCTicket1; var l_List : TmyIntegerList; begin l_List := TmyIntegerList.Create; try l_List.Add(Random(54365)); l_List.Delete(0); finally FreeAndNil(l_List); end;//try..finally end;
И убеждаемся, что он таки НЕ проходит!
Что делать?
Правим код проектного класса:
procedure TmyIntegerList.Delete(anIndex: IndexType); begin if (anIndex < 0) OR (anIndex >= Self.Count) then // - ну нечего тут удалять Exit else if (anIndex = Self.Count - 1) then SetLength(f_Items, Self.Count - 1) else Assert(false, 'Ну не знаю я что тут делать'); end;
Запускаем тесты - они - проходят. Сделали ли мы свою работу? НЕ ЗНАЮ. Наверное...
Теперь давайте предположим, что ГК нашла ещё одну ошибку:
... to be continued ...
Продолжать? Или "идея и так понятна"?
Интересная статья, спасибо!
ОтветитьУдалитьПожалуйста :-)
УдалитьВ некотором смысле юнит-тесты решают абсолютно те же задачи, что и статическая типизация. Возможно, поэтому они наиболее прижились как раз там, где статической типизации нет.
ОтветитьУдалитьЧто такое статическая типизация? Это некое описание требований к коду, на соответствие котором компилятор будет наш код проверять. Что такое юнит-тесты? Это опять требования к коду, на соответствие которым код будет проверяться.
Если воспринимать юнит-тесты именно так, то исчезают все противоречия. С чего начинают разработку программы в языках со статической типизацией? С описания типов! С чего нужно начинать разработку программы в методологии TDD? С написания тестов! Так что это именно architecture first в обоих случаях.
Это выглядит не очевидно и разобщено, но во всем виноваты недостаточно развитые языковые средства, которыми мы пользуемся. Но тем не менее, в каком бы виде мы бы не описывали требования к входным и выходным данным некоторой функции, мы описываем ее ТИП и ничего более.
Роман, ты ВО МНОГОМ ПРАВ!
УдалитьТы даже заставил меня ПО ИНОМУ взглянуть на процесс разработки и тестирования.
СПАСИБО!
Пред-условий и пост-условия - это - КРУТО!
И они - ВАЖНЫ и РАБОТАЮТ.
Но они НЕ ЗАМЕНЯЮТ тестов.
Скажу так - пред-условие и пост-условие - это КОНТРАКТ. А ТЕСТ - это "точка входа" для проверки контракта.
БЕЗ "точки входа" КОНТРАКТ может оказаться НЕ ПРОВЕРЕННЫМ.
Ну ведь не на "реальных же пользователях тестировать".
Если "не донёс" - могу развить тему. Если интересно.
Не, я понимаю разницу между контрактами и тестами. Это безусловно разные вещи, но тем не менее между ними много общего :)
Удалить"Не, я понимаю разницу между контрактами и тестами. Это безусловно разные вещи, но тем не менее между ними много общего :)"
ОтветитьУдалитьБЕЗУСЛОВНО! :-)
Есть, кстати, системы, позволяющие автоматически генерировать юнит-тесты по контактам. Но все это очень экспериментально пока.
Удалитьну да... ну да... Только я лично склоняюсь к выведению контрактов и тестов из модели и требований при помощи кодогенерации... И расширений к DUnit с использованием атрибутов.
ОтветитьУдалить