Показаны сообщения с ярлыком тесты вообще. Показать все сообщения
Показаны сообщения с ярлыком тесты вообще. Показать все сообщения

пятница, 12 июля 2013 г.

Обобщаем тесты списков

Предыдущая серия была тут - http://18delphi.blogspot.com/2013/07/blog-post_8789.html

Теперь обобщим приведённые там тесты.

Как обычно.

Модель:

Тут добавился "примесный тест" - ListTest, а все остальные тесты от него - наследуются и определяют тип конкретного списка, который надо тестировать. Этот тест нам сильно пригодится, когда мы будем расширять функциональность контейнеров. Вместе с функциональностью абстрактных контейнеров будем расширять и функциональность примесного теста. Таким образом - КОНКРЕТНЫЕ тесты - "сами" будут расширяться.

Код:

ListTest.imp.pas:
{$IfNDef ListTest_imp}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Библиотека "SandBoxTest"
// Модуль: "ListTest.imp.pas"
// Родные Delphi интерфейсы (.pas)
// Generated from UML model, root element: TestCaseMixIn::Class Shared Delphi Sand Box::SandBoxTest::FinalContainersTests::ListTest
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

{$Define ListTest_imp}
{$If defined(nsTest)}
 _ListTest_ = class(TTestCase)
 published
 // published methods
   procedure DoIt;
   procedure TestTwoLists;
 end;//_ListTest_
{$IfEnd} //nsTest

{$Else ListTest_imp}

{$If defined(nsTest)}

// start class _ListTest_

procedure _ListTest_.DoIt;
//#UC START# *51DEB319037C_51E03FC80111_var*
const
 cCount = 1000;
var
 l_List : _ListType_;
 l_Count : IndexType;
 l_Index : IndexType;
//#UC END# *51DEB319037C_51E03FC80111_var*
begin
//#UC START# *51DEB319037C_51E03FC80111_impl*
 l_List := _ListType_.Create;
 try
  l_List.Count := cCount;
  Check(l_List.Count = cCount);
  Check(l_List.Capacity >= cCount);

  for l_Index := 0 to l_List.Count - 1 do
   Check(l_List[l_Index] = 0);

  l_Count := Random(cCount);
  l_List.Count := l_Count;
  Check(l_List.Count = l_Count, Format('Выделяли %d элементов. Count = %d', [l_Count, l_List.Count]));
  Check(l_List.Capacity >= l_Count, Format('Выделяли %d элементов. Capacity = %d', [l_Count, l_List.Capacity]));

  for l_Index := 0 to l_List.Count - 1 do
   Check(l_List[l_Index] = 0);
   
 finally
  FreeAndNil(l_List);
 end;//try..finally
//#UC END# *51DEB319037C_51E03FC80111_impl*
end;//_ListTest_.DoIt

procedure _ListTest_.TestTwoLists;
//#UC START# *51DED6FC03C1_51E03FC80111_var*
const
 cCount = 1000;
var
 l_A : _ListType_;
 l_B : _ListType_;
 l_Index : IndexType;
 l_Value : _ItemType_;
 l_Max   : _ItemType_;
//#UC END# *51DED6FC03C1_51E03FC80111_var*
begin
//#UC START# *51DED6FC03C1_51E03FC80111_impl*
 l_A := _ListType_.Create;
 try
  l_B := _ListType_.Create;
  try
   l_A.Count := cCount;
   for l_Index := 0 to l_A.Count - 1 do
   begin
    l_Value := Random(1000);
    l_A[l_Index] := l_Value;
    Check(l_A[l_Index] = l_Value);
   end;//for l_Index

   l_B.Count := l_A.Count;
   for l_Index := 0 to l_A.Count - 1 do
   begin
    l_B[l_Index] := l_A[l_Index];
   end;//for l_Index

   for l_Index := 0 to l_A.Count - 1 do
   begin
    Check(l_B[l_Index] = l_A[l_Index]);
   end;//for l_Index
  finally
   FreeAndNil(l_B);
  end;//try..finally
 finally
  FreeAndNil(l_A);
 end;//try..finally
//#UC END# *51DED6FC03C1_51E03FC80111_impl*
end;//_ListTest_.TestTwoLists

{$IfEnd} //nsTest

{$EndIf ListTest_imp}

IntegerListTest.pas:
unit IntegerListTest;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Библиотека "SandBoxTest"
// Модуль: "IntegerListTest.pas"
// Родные Delphi интерфейсы (.pas)
// Generated from UML model, root element: TestCase::Class Shared Delphi Sand Box::SandBoxTest::FinalContainersTests::IntegerListTest
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

{$Include .\sbtDefine.inc}

interface

{$If defined(nsTest)}
uses
  TestFrameWork,
  IntegerList
  ;
{$IfEnd} //nsTest

{$If defined(nsTest)}
type
 _ListType_ = TIntegerList;
 {$Include .\ListTest.imp.pas}
 TIntegerListTest = class(_ListTest_)
 end;//TIntegerListTest
{$IfEnd} //nsTest

implementation

{$If defined(nsTest)}
uses
  SysUtils
  ;
{$IfEnd} //nsTest

{$If defined(nsTest)}

type _Instance_R_ = TIntegerListTest;
type _ListTest_R_ = TIntegerListTest;

{$Include .\ListTest.imp.pas}


{$IfEnd} //nsTest

initialization
 TestFramework.RegisterTest(TIntegerListTest.Suite);
end.

ByteListTest.pas:
unit ByteList;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Библиотека "SandBox"
// Модуль: "ByteList.pas"
// Родные Delphi интерфейсы (.pas)
// Generated from UML model, root element: SimpleClass::Class Shared Delphi Sand Box::SandBox::FinalContainers::TByteList
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

{$Include .\sbDefine.inc}

interface

uses
  Refcounted,
  Classes,
  l3PtrLoc
  ;

type
 _ItemType_ = Byte;
 _AtomicList_Parent_ = TRefcounted;
 {$Include .\AtomicList.imp.pas}
 TByteList = class(_AtomicList_)
 end;//TByteList

implementation

uses
  RTLConsts,
  l3MemorySizeUtils
  ;

type _Instance_R_ = TByteList;
type _AtomicList_R_ = TByteList;

{$Include .\AtomicList.imp.pas}

end.

Int64ListTest.pas:
unit Int64ListTest;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Библиотека "SandBoxTest"
// Модуль: "Int64ListTest.pas"
// Родные Delphi интерфейсы (.pas)
// Generated from UML model, root element: TestCase::Class Shared Delphi Sand Box::SandBoxTest::FinalContainersTests::Int64ListTest
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

{$Include .\sbtDefine.inc}

interface

{$If defined(nsTest)}
uses
  TestFrameWork,
  Int64List
  ;
{$IfEnd} //nsTest

{$If defined(nsTest)}
type
 _ListType_ = TInt64List;
 {$Include .\ListTest.imp.pas}
 TInt64ListTest = class(_ListTest_)
 end;//TInt64ListTest
{$IfEnd} //nsTest

implementation

{$If defined(nsTest)}
uses
  SysUtils
  ;
{$IfEnd} //nsTest

{$If defined(nsTest)}

type _Instance_R_ = TInt64ListTest;
type _ListTest_R_ = TInt64ListTest;

{$Include .\ListTest.imp.pas}


{$IfEnd} //nsTest

initialization
 TestFramework.RegisterTest(TInt64ListTest.Suite);
end.

Код тут - http://sourceforge.net/p/rumtmarc/code-0/19/tree/trunk/Blogger/SandBox и http://sourceforge.net/p/rumtmarc/code-0/19/tree/trunk/Blogger/SandBoxTest

четверг, 20 июня 2013 г.

Ещё буквально два слова про тесты

Просто сегодня услышал примерно то же самое от коллег.... Посему - повторюсь.

Пишите тесты на ВСЕ возможные "технические" предложения (в смысле sentence, а не proposal) из ТЗ. На ВСЕ которые ведут к той или иной логике кода. Насколько это ВОЗМОЖНО в вашем фреймворке и с вашим "порогом знаний" о тестах. Насколько возможно это сделать БЫСТРО. Если что-то не получается БЫСТРО - не стоит мучатся. Иначе вы будете говорить - "тесты замедляют мою работу". А я хочу добиться - РОВНО ОБРАТНОГО. (напишите один тест, два, пять, десять.. потом - горизонты - расширятся. Но это - вопрос времени)

Написано - "такие входные данные должны давать такие выходные" - пишем тест. Написано - "таких данных не бывает" - пишем тест (с assert). Написано "должна быть кнопка" - пишем тест. Написано - "из этого следует это" - пишем тест.

Вы удивитесь - насколько легко и быстро вы научитесь находить неточности и несогласованности в различных ТЗ.

Я ЛИЧНО - ещё ни разу не видел "идеальных" ТЗ. Особенно "в исторической перспективе". Когда одно ТЗ - влияет на другое. Написанное "скажем лет пять назад". И видимо это - "объективная реальность данная нам в ощущениях".

Мы не можем "сделать" или "добиться "идеального" ТЗ. Особенно "в исторической перспективе". Но мы можем ВАЛИДИРОВАТЬ эту "перспективу". И ВАЛИДИРОВАТЬ непротиворичивость РАЗНЫХ ТЗ.

Попробуйте. Я практически уверен, что вам понравится. Если вы конечно попробуете.

P.S. Ещё про тесты вот тут - http://18delphi.blogspot.ru/2013/05/blog-post_3529.html и вот тут - http://18delphi.blogspot.com/2013/05/blog-post_7.html.

вторник, 4 июня 2013 г.

Добрались мы до той ситуации, когда тесты не проходят на ВСЕХ машинах

Добрались мы до той ситуации, когда тесты не проходят на ВСЕХ машинах.

И при этом - КОД ведёт себя - ПРАВИЛЬНО. Ну насколько он может. Себя "правильно вести".

Пока додумались только до "квантификаторов" типа EtalonNeedsComputerName (самое волюнтаристское), EtalonDependsFromOS, EtalonDependsFrom64System.

P.S. И далеко не ВСЁ кстати зависит от нас. Мы например выяснили один простой факт, что в 32-битном приложении exception под OS 64 "тупо" не всплывает из Windows-процедуры. WinProc. В итоге - то, что является ошибкой под 32 бита - отнюдь не является таковой под 64 бита. Мы что-то не так делаем? Руки у нас кривые?

"Анти-тест" vs. "этот код должен падать (поднимать исключение) по ТЗ"

"Анти-тест" vs. "этот код должен падать (поднимать исключение) по ТЗ". Грань ясна? Проблема понятна? Тут хотелось бы подискутировать....

Понятно, что "анти-тест" - это ВРЕМЕННАЯ мера, до тех пор пока ошибка не будет исправлена. А второй вариант - "высечен в бронзе". Ну ДОЛЖЕН код "падать" по ТЗ. Ну граничные условия не те. Или пользовательский ввод не тот.

И хотя - поведение "системы тестирования" в ОБОИХ случаях - может быть ОЧЕНЬ ПОХОЖИМ - но на уровне разработчика тестов - эти два случая - КАРДИНАЛЬНО различаются. И разработчик тестов должен понимать это "внутренним чутьём".

По-моему - это - интересная проблема.

четверг, 9 мая 2013 г.

Рост инфраструктуры проектов (по нарастанию потребностей)


  1. Еженедельные архивы исходного кода.
  2. "Бумажные" диаграммы.
  3. Ручное тестирование выпусков.
  4. Соглашение об оформлении кода.
  5. Подсчёт ссылок на объекты.
  6. Тестовые наборы "на коленке" и их ручное "протыкивание".
  7. Code Review.
  8. CVS
  9. Инструкция программирования и оформления исходного кода.
  10. Списки ToDo.
  11. Логирование ошибок с выводом стека.
  12. Учимся пользоваться Araxis Merge или его аналогами (jfc, tfc, afc, на худой конец - fc).
  13. Clear Quest или любая другая "плоская" база ошибок.
  14. Проверка утечек (в частности через подсчёт ссылок).
  15. "Ручные" ежедневные отчёты в почте.
  16. Usecase'ы.
  17. Документирование исправленных ошибок прямо в коде.
  18. High Level Test Cases.
  19. Ночные сборки на bat-файлах.
  20. "MVC".
  21. Рефакторинг.
  22. Сбор логов от пользователей.
  23. wiki-подобная база знаний и база ошибок.
  24. "Код у нас общий".
  25. Code Review в wiki с использованием обсуждений в электронном виде.
  26. Документирование исправленных ошибок со ссылками на wiki.
  27. Ночные сборки на продвинутых "скриптах" (например ant) с публикацией отчётов в wiki.
  28. Автоматизированные ежедневные отчёты в wiki.
  29. Автоматизированное тестирование чёрного ящика, через GUI (например Test Complete или разные другие роботы).
  30. Детализация требований.
  31. Применение шаблонов проектирования от GoF (и не только).
  32. UML.
  33. Кодогенерация из UML.
  34. Соглашения об унифицированной настройке среды.
  35. Нагрузочное тестирование.
  36. Проектнозависимые стереотипы для кодогенерации.
  37. Рефакторинг под влиянием UML.
  38. Тесты библиотек.
  39. Стереотипы для тестов.
  40. Расставлены определённые технологические точки над i с отделом маркетинга.
  41. Механизм контрольных точек.
  42. Ежедневные прогоны тестов.
  43. Рефакторинг при наличии тестов.
  44. Механизм "эталонов" для тестов.
  45. Механизм размножения тестов относительно тестовых наборов.
  46. GUI-тесты.
  47. Скриптованные тесты.
  48. Специализированные стереотипы для шаблонов проектирования (например GoF).
  49. Ежедневные прогоны тестов и их разбор специально выделенными людьми.
  50. Написание тестов по результатам УЖЕ найденных ошибок, специально выделенными людьми.
  51. Unit-тестирование.
  52. Выделение аспектов в примеси.
  53. Интеграционное тестирование.
  54. Трассировка требований в тесты.
  55. Автоматическая публикация новых ошибок и "возрождение" старых вследствие ежедневного прогона тестов после ночной сборки.


среда, 8 мая 2013 г.

Про тесты, но не для Delphi

http://blog.byndyu.ru/2010/01/tdd.html#.UYmGQaKIcUw

Интереснее всего читать - комментарии.

Ну и ещё одна хорошая статья про TDD

http://www.thalion.kiev.ua/idx.php/17/351/article/
Называется - "Ошибки начинающих TDD-практиков".

Опубликована в 2007-м году. Т.е. примерно тогда, когда я только начал заниматься всерьёз темой тестов. Жаль, что я её тогда не читал.

Ещё хорошая статья про тесты

http://www.itshop.ru/Prostoe-napisanie-testov-eto-ne-TDD/l9i31547
Называется - "Простое написание тестов - это не TDD!".

СТОИТ ЧИТАТЬ.

Но только НЕ СТОИТ делать вывод - что не надо писать тесты без TDD. Ничто не надо делать догмой. Лучше кривой и косой ОДИН тест, чем несуществующий TDD.

И ОПЯТЬ - не могу удержаться от цитирования:

"
Изменения без страха
TDD позволяет не бояться изменений. Я работал во множестве проектов, прежде чем ощутил преимущества TDD. Оглядываясь назад, я могу сказать, что всегда боялся вносить изменения в код. Я проводил много времени, тестируя приложение вручную для того, чтобы убедиться что я не сломал его работоспособность. С приходом TDD этот страх ушёл - функциональность всегда покрыта тестами, и ты способен получить почти моментальный отклик от системы или любой её части. В то же время отсутствие страха перед рефакторингом повышает внутреннее качество твоей программы, что положительно сказывается и на внешнем качестве."

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


вторник, 7 мая 2013 г.

"Всё уже придумано до нас"

http://delphi.frantic.im/delphi-tdd/

Надеюсь, что автор не обидится, если я его процитирую:

"Беда вводных статей по тестированию кода в том, что они описывают правила написания тестов как догму: “делайте так, только так и никак иначе, потому что так надо”. Догма не гибкая и убивает креативность, а написание тестов требует гибкости и креативности. В этом случае лучше принять правило кармы: “делайте добрые дела и с вами будут случаться хорошие вещи, делайте их так, как умеете и так, как вам это нравится”. Эту формулировки мысли я почерпнул из замечательной книги –The Way of Testivus (очень рекомендую к прочтению). Перевод основных ее мыслей – в конце статьи.
Нужно понимать, что идеально правильного теста не существует. Вспомните свой код, который казался Вам вершиной инженерной мысли год или два назад. Наверняка сейчас, набравшись больше опыта, Вы его таким уже не считаете. С тестами та же история – идеальный тест сегодня уже не будет таким для Вас завтра. Поэтому, не пытайтесь сделать все безупречно! Начинайте с простого! Экспериментируйте! Развивайте свой код и свои тесты итерациями, каждый раз улучшая их структуру и содержание. А если Вы сталкиваетесь с трудностями – почитайте советы опытных разработчиков, наверняка они через это уже прошли.
Также забудьте о 100% покрытии кода тестами (о котором так любят говорить задроты). Оно просто того не стоит. Здесь отлично работает правило 80/20. Всего 20% усилий сможет дать Вам проверку 80% Вашего кода (а это намного больше чем вообще ничего). К остальным 80% усилий, на мой взгляд, относится тестирование графического интерфейса. Если Вы построили приложение так, что UI используется только для передачи данных во внутрь приложения и для вывода результатов, достаточно протестировать только этот внутренний движок.
И немого информации для тех, кто считает тестирование слишком затратным (наверняка, такое ошибочное мнение складывается только из-за небольших трудностей, которые тяжело преодолеть думая о тестах как о догме). Написание теста действительно занимает больше времени, чем ручная проверка и исправление проблем через отладчик, прямо на месте. Но если посмотреть в перспективе – это очень выгодное вложение. Автоматический тест выполняется за доли секунд, ему никогда не лень и он всегда внимателен (в отличии от человека). Если одну и ту же часть программы Вы проверяете 5-7 раз вручную, уже на этом этапе тест бы себя окупил. А если подумать о регрессии (ошибка в одном месте ломает что-то в другом) тесты – незаменимая вещь."
Я лично - готов подписаться под КАЖДОЙ буквой из приведённой цитаты. Лучше - и не скажешь.

Опять же ЗНАКОМО на все 100%:

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

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

Понимаю, что перебираю с цитированием, но вот то - до чего я дошёл (и не раз озвучивал устно), но так и не смог написать и то, что "за меня" написал автор статьи:

"Советы из личного опыта:
  1. Делайте тесты «плоскими», без if, while и т.д. В тесте не должно быть логики.
  2. Запускайте тесты как можно чаще. Слишком медленные можно отключить в DUnit (если то, над чем Вы работаете, на них не влияет).
  3. Начинайте с простого. Да, в интернете есть много хитрых инструментов для тестирования, не повышайте сложность без необходимости.
  4. Если используете Continuous Integration, включите запуск консольного варианта тестового проекта в скрипт сборки.
  5. Тестирование не сделает Ваш проект успешным и не избавит от 100% багов. Но очень в этом поможет.
Советы от Testivus (вольный перевод)
  1. Если пишете код, пишите тесты
  2. Не воспринимайте тестирование как догму
  3. Воспринимайте тестирование как карму
  4. Думайте о коде и тесте как об одном целом
  5. Тестирование важней чем ограничивающие правила
  6. Лучше всего писать тесты когда код еще горячий
  7. Тесты, которые не запускаются систематично, пустая трата времени
  8. Неидеальный тест сегодня лучше, чем идеальный тест когда-то в будущем
  9. Некрасивый тест лучше чем вообще без теста
  10. Хорошие тесты проваливаются"
И КРАЙНЕ советую - прочитать ВСЕ комментарии внизу статьи.

Там ещё маленький холиварчик про FreeAndNil. Я лично предпочитаю использовать его - ВЕЗДЕ.

Update. Один мой коллега, который ПЛОТНО занимается Unit-тестированием, после прочтения данной статьи написал лишь - "Да, сильно.". Это многое значит.

Ещё один подход про тесты

Вот спрашивают:


Тесты, тесты.....НО КАК ПИСАТЬ ИХ??? Хоть убей не догоняю!Когда-то так не смог понять что такое производное и с чем его едять. В одной книжке увидел черту, который касается к графику фунции и пересекающийся ось Х, и ЧУДО! Все стало понятно!Оказывается, я не туда шел понять. Вот и с тестами видимо так.

Ну давайте по-порядку. Вы например DUnit видели? Да и потом, что значит "КАК"? Вы код "КАК" пишете? Тесты - тот же код. Только в несколько иной инфраструктуре. С иными целями и задачами. Цель кода - реализовать ТЗ, цель теста - проверить корректность кода относительно каких-то условий, например полноты реализации ТЗ. Если вы ждёте от меня "серебряной" пули - то у меня её - нет. Ни у кого - нет. Надо понять одну простую вещь для начала. Что тесты надо писать. И запускать. И разбирать результаты прогонов. И взять для себя это в привычку. Когда тестов один, два, десяток - выигрыша не видно. Когда их набирается достаточно (например переваливает за сотню) - выигрыш становится виден. Он нарастает как снежный ком. Тесты всё чаще и чаще сигнализируют об ошибках (не забываем правда и о ложных срабатываниях).

Вот тут - http://18delphi.blogspot.com/2013/05/blog-post_7.html  - я перечислил случаи - в которых бы я писал бы тесты (без фанатизма конечно же).

Какой из этих случаев непонятен? Какой привести пример? Может быть у вас есть какой-то конкретный случай? Огласите его. Что мол "вот тут - как писать тест и какой". Может так проще?


В своих предыдущих постах - я приводил код не одного теста. Там что-то непонятно?


Что до производной - мне лично график - ни о чём не говорит :-) Ну или не говорил. Мне ближе было понятие "скорости". Сколько людей - столько ощущений.


P.S. может быть понимания добавит тот факт, что должна быть "база тестов" (отчасти перекликающаяся с базой ошибок)? В которой имеем список тестов с их описанием. Хотя бы в косвенном виде.


P.P.S. Как эволюционно выращивается проектная инфраструктура (она не появляется здесь и сейчас по мановению волшебной палочки), так эволюционно выращивается и тестовая инфраструктура. Сначала тесты на наиболее критичные участки, ну где больше всего ошибок находится (у меня это был предварительный просмотр печати). Потом тесты на смежные области. Потом глядишь сделали работу с эталонными файлами. Потом реализовали (или взяли чужие) mock'и. Потом добрались до интеграционных тестов. Потом неожиданно осознали нужность и полезность простейших атомарных тестов. Потом вдруг решили тестировать GUI. Ну потому, что некоторые участки кода по-другому не протестируешь. Потом взялись за тесты производительности. Ну как-то так. Эволюционно. За "пару ней" - не получится. Но каждый следующий шаг будет легче и интереснее. А далее уже инфраструктура будет наследоваться от проекта к проекту. Что-то добавляться, что-то убираться. Но вырастет некий "скелет". Более менее универсальный. Для круга ваших задач. А не "для всех задач в природе вообще".

Тесты влияют на архитектуру

Они позволяют "разрезать" систему в несколько иной плоскости. Если вдруг базовые классы вызывают диалоговые окна или другой интерактив с пользователем - тесты очень хорошо сигнализируют об этом. И позволяют провести "водораздел" по-другому. В общем - MVC, но MVC через тесты. Как-то так. Ну это на пальцах.

Update. А вот и внешние ссылки:
http://www.itshop.ru/Prostoe-napisanie-testov-eto-ne-TDD/l9i31547
http://delphi.frantic.im/delphi-tdd/

Снова о тестах. Другими словами

Пишите тесты. Обязательно пишите. Это поможет вам чувствовать уверенность в завтрашнем дне. Чтобы быть уверенным в том, что код и вся архитектура не развалится завтра при случайной правке базового класса или внесении правок в ТЗ.

Пишите тесты к:
1. Пользовательским сценариям (UseCase) из ТЗ.
2. Пунктам детализирующим ТЗ. Такие тесты позволят кроме проверки пунктов ТЗ ещё и находить противоречия в ТЗ.
3. Найденным ошибкам. (!)
4. Стабильно повторяющимся ошибкам. (!!)
5. Ошибкам, которые "вернулись уже не раз". (!!!)
6. Атомарные тесты к базовым классам. Типа: List.Add(10); Check(List[0] = 10); Такие тесты помогут вам при переходе с одной платформы на другую.
7. Unit-тесты к самым базовым классам.
8. Тесты, которые помогают "быстро влиться в разработку" пока несуществующего проекта. Такие тесты позволят вам "не ждать коллег".
9. Ошибкам стандартных библиотек, которые вы поправили. Эти тесты помогут вам проще переходить на новые версии этих библиотек.
10. К нестандартным решениям и "копанию в кишочках" своего и чужого кода.
11. Отдельно к различным преобразованиям из одного формата в другой. Такие тесты легко пишутся через mock'и или эталонные файлы. Конечно их надо иметь достаточный набор.
12. Если вы что-то долго и нудно оптимизируете по памяти и/или быстродействию. Пишите тест. Он поможет вам уследить за ситуацией, когда оптимизация вышла за желаемые рамки.
13. Граничным условиям и неверному пользовательскому вводу.
14. Если у вас с коллегами возникли разногласия о поведении системы на "границе сфер влияния".
15. Коду других коллег, которые уже не с вами. Особенно когда он вызвал вопросы типа - "а что это такое", "а может быть тут внести такую правку".

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

Одно только введение "контрольных точек" позволяет оценить систему не с точки зрения "монолита", а с точки зрения "а что бы было бы" если бы вот тут можно было бы снять показания или внести запланированные помехи.

Насчёт "тесты замедляют процесс разработки" - прокомментирую лишь в таком ключе - у меня - не замедляют, даже иногда и ускоряют (см. например пункт 8). Но при этом тесты дают гораздо большую свободу манёвра. Я безбоязненно правлю самые базовые классы и код коллег, которых со мною уже рядом нет. Я всегда знаю, что я могу прогнать набор тестов и посмотреть - "а что же будет". И мне приходится гораздо меньше сомневаться - стоит вносить ту или иную правку в проектный код или нет.

Попробуйте. Может быть вам понравится.

Update. Опять же читаем статью - http://delphi.frantic.im/delphi-tdd/ и КОММЕНТАРИИ к ней.

вторник, 30 апреля 2013 г.

Продолжение о тестах

Первая серия была тут - http://18delphi.blogspot.com/2013/04/blog-post_5863.html

Пишет мой коллега:


Как справедливо замечено, есть действительно 2 основных направления автоматизированного тестирования: проверять фиксированные сценарии (в интерфейсе) или просто следить за тоннами активизируемого контролами функционала. Каждое имеет свои преимущества и недостатки. Но на данном этапе было принято решение проверять только сценарии уже найденных и написанных ошибок. И это, на мой взгляд, разумно, т.к. ошибки эти  находились в разных прецедентах системы. Каждый новый написанный тест охватывает небольшую толику функционала. Но этих тестов становится все больше. И чем их больше, тем выше вероятность, что ошибка будет не пропущена. Для поиска ошибок с сильно ограниченными трудовыми ресурсами – это оптимальный вариант.
Таким образом, избегая зацикливания на одном объекте системы, тесты охватывают всю систему паутиной. И стоит “задеть” одну нить – вся паутина задрожит и даст знать, что что-то случилось. С одной стороны, это замечательно, что тесты перестали проходить. Значит, они работают и нужно проверить изменения в системе. С другой, это ужасно неудобно с точки зрения локализации проблемы. Очень редко, ну очень, возвращается старая ошибка. Тест специально заточен под то, чтобы аккуратно зафиксировать ошибку и уведомить об этом тестировщика. Но, как правило, ломается что-то другое, “по-соседству”. Тест это честно находит, но часто не может четко показать причину ошибки. На эти случаи вставляются ASSERT ы, но всех возможных ошибок нельзя учесть (тогда написание одного теста будет занимать очень долгое время).
Сами тесты я разделяю на следующие группы:
- Уникальные тесты (которые никогда не пересекаются с другими тестами и проверяют область, которую не проверяют другие тесты);
- Тесты, на которые могут повлиять другие тесты (и это тоже проблема, которая решается “качественным” кодом). Например, любые тесты с поиском (от другого теста может остаться фильтр, который изменит результат).
- Тесты – близнецы (разница в которых может составлять и одну строку, и один символ).
Значит, тесты можно делать либо максимально универсальными, либо максимально уникальными. Но в сложных программах путь универсализации – ошибочен. Первое время я старался писать тесты и слова для них с запасом для будущих тестов. Специально “опускал” некоторые части кода. Например:

: “Выделить весь текст в документе”
“Зайти в документ”
“Выделить весь текст”
“Сравнить выделенный текст с эталоном”
; \\ “Выделить весь текст в документе”

То есть, я знаю, что при входе в документ фокус стоит в тексте. И опускаю код “Установить фокус в текст”. Значит, в этом тесте проверяется не только результат выделения, но и то, что фокус оказывается в документе (косвенно). Но знаю также, что есть другой тест, который проверяет именно то, где находится фокус:

: “Проверить, что фокус после открытия документа находится в тексте”
“Зайти в документ”
“Проверить, в каком контроле стоит фокус”
; \\“Проверить, что фокус после открытия документа находится в тексте”

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

: “Выделить весь текст в документе”
“Зайти в документ”
“Установить фокус в текст”
“Выделить весь текст”
“Сравнить выделенный текст с эталоном”
; \\ “Выделить весь текст в документе”

  Такой подход, в будущем, упростит разбор ошибок. Т.е. стараться сделать каждый тест уникальным, проверяющим только описанную ошибку – правильно. К этому нужно стремиться, но без фанатизма.
Теоретически, если все тесты будут работать по такому принципу, не нужен будет специальный человек, который обучен работать с тестами. Потому что в каждом непрошедшем тесте будет подробно описана ошибка. Другие тесты пройдут успешно. Любой пользователь сможет написать ошибку, приведя номер теста и сообщение найденной ошибки. Но это – идеальная система. Добиться, такого результата, думаю, невозможно. Хотя идти к нему стоит.
Если рассматривать мою работу, с точки зрения результативности, то я бы оценил её как среднюю. Ошибки исправно находятся, но есть несколько причин, по которым её нельзя назвать хорошей.
Во-первых, это малое количество тестов, из-за чего вероятность пропустить ошибку очень высока. Чтобы убрать эту причину, нужно лишь время (очень много времени).
Во-вторых, авторство всех тестов принадлежит трем людям. Иногда (в последнее время всё реже) приходится разбираться, почему же тест написан так, когда проще было сделать по-другому. Это частично связано с тем, что ранний функционал не обеспечивал простоту и понятность кода. А также, есть свои “секреты” в работе системы (и тестового приложения). Конечно, очень полезно расставлять комментарии в коде именно ради таких моментов. Но каюсь, добавляю комментарии редко, держу особенности работы в голове. Иногда они забываются, но если с проблемой уже сталкивался, вспоминается она очень быстро. Т.е. коду не хватает формализации.
В-третьих, человеческий фактор. У нас налажена тройная проверка готового теста: первый раз его проверяет автор при отладке, далее тест смотрит разработчик и, если все работает корректно и проверяет, что нужно, тест попадает опять к автору на последнюю проверку и закрытие. При такой проверке маловероятна возможность пропустить ошибку в самом тесте, но есть возможность самим кодом проверять не совсем верный сценарий (не повторяющий ошибку, т.к. тесты пишутся и для уже исправленных ошибок). Бороться с этой причиной проще остальных. Нужно лишь прилагать максимум внимания и стараться выбросить мысли о других ошибках из головы. Полезно также посмотреть точный сценарий ошибки на сборке, на которой она воспроизводилась.
В-четвертых, можно ли утверждать, что когда-то написанный и отлаженный тест будет корректно проверять именно то, что нужно, через 2-3 года. Нельзя. Нужно иногда проводить ревизию тестов (хотя бы выборочно).
Есть еще несколько малозначимых причин. Их, думаю, можно не указывать.
Теперь рассмотрим, какой же человек может справиться с работой с автоматическими тестами.
1) Он должен знать термины и аббревиатуры, принятые в системе.
2) Нужны базовые знания любого языка программирования (не обязательно Delphi или FORTH).
3) Нужна аккуратность, вдумчивость и усидчивость.
То есть, критериев для кандидата не так уж и много. Но их можно сделать еще меньше, упростив работу по написанию тестов. Сделать это очень просто.
Приведу пример из жизни. Однажды мне пришлось поработать в Тверьстате (Территориальный Орган Федеральной Службы государственной статистики). Первая половина работы не имеет к данному рассказу никакого отношения (контроль и правка результатов сканирования). Зато вторая стала очень поучительной. Её суть заключалась в правке Excel – документов (оптимизировать размер полей под печать на минимальном количестве листов). Задача очень простая. Но однотипных документов были тысячи, и выполнять все действия вручную было несерьезно. Была нужна автоматизация. И она была создана одним очень талантливым студентом.
Процесс нашей работы имел вид:
1 день) Ознакомились с заданием, вручную сделали несколько документов.
2 день) Кто-то вспомнил, что в Excel есть макросы. Создав один макрос, его можно указывать из любого документа. Исправили несколько сотен документов.
3 день) В. Е. написал программу, для работы которой нужно: указать папку с документами, указать путь к документу с макросом и его имя, и нажать кнопку Пуск. Программа открывает по порядку документы из папки, применяет макрос, сохраняет документ и закрывает его. Вся работа свелась созданию одного макроса в начале рабочего дня. Не понимаю, почему штатный состав программистов не создал для Тверьстата подобную программу много лет назад (мы были практиканты).
Но речь сейчас не об этом, а о макросах.  Пользователь понятия не имеет какой код используется, когда он записывает сценарий. Он лишь выполняет конечную последовательность действий, которую ему необходимо неоднократно выполнить. Для этого не нужны сверхзнания или специальная подготовка, достаточно будет лишь терминов системы.
То есть, на мой взгляд, написание основной группы тестов должно рано или поздно свестись к работе, схожей с написанием макросов: выполняем действия, система их запоминает и в конце сравнивает результат по каким-либо критериям. Это сделает написание тестов более простым и гораздо более быстрым. Если потребуется написать тест под каждое требование из ТЗ или автоматизировать проверку уже созданных тесткейсов – данный подход поможет это сделать. Ручное написание подобного количества тестов заняло бы годы.
Задачей разработчиков здесь бы стало максимально формализовать код (который записывает алгоритм действий) и результат работы тестов, а также подготовить специалиста, который бы разбирал результаты прогона тестов. Также, этот человек должен будет заниматься написанием более сложных тестов, в которых не получается работать с четким алгоритмом.
Не уверен, что на данном этапе такую систему можно реализовать. Также не уверен, что она окажется идеальной по поиску ошибок и гибкой (при ее использовании). Но данную идею можно додумать. И тогда она может оказаться весьма эффективной.


Трофимов Андрей

пятница, 26 апреля 2013 г.

Проезд по памяти в VCL



procedure TControlCanvas.CreateHandle;
...
 
  if FControl = nil then
    inherited CreateHandle
  else
  begin
    // Creation of a window could trigger messages that require
    // the canvas to have a valid handle. Prevents creating two DCs.
    if (FDeviceContext = 0) and (FControl is TWinControl) then
      TWinControl(FControl).HandleNeeded;
    if FDeviceContext = 0 then
    begin
      with CanvasList.LockList do
      try
        if Count >= CanvasListCacheSize then FreeDeviceContext;
        FDeviceContext := FControl.GetDeviceContext(FWindowHandle);
        if (IndexOf(Self) < 0) then {V} // - это я добавил
         Add(Self);
      finally
        CanvasList.UnlockList;
      end;
    end;
    Handle := FDeviceContext;
    UpdateTextFlags;
  end;
...
end;


Без этого падает тут:


// Free the first available device context
procedure FreeDeviceContext;
var
  I: Integer;
begin
  with CanvasList.LockList do
  try
    for I := 0 to Count-1 do
      with TControlCanvas(Items[I]) do
        if TryLock then
        try
          FreeHandle; // - тут
          Exit;
        finally
          Unlock;
        end;
  finally
    CanvasList.UnlockList;
  end;
end;

Или тут:



procedure FreeDeviceContexts;
var
  I: Integer;
begin
  with CanvasList.LockList do
  try
    for I := Count-1 downto 0 do
      with TControlCanvas(Items[I]) do
        if TryLock then
        try
          FreeHandle; // - тут
        finally
          Unlock;
        end;
  finally
    CanvasList.UnlockList;
  end;
end;


Как повезёт.

Что в общем-то и понятно - одна канва может несколько раз попасть в CanvasList.

Сейчас попробую найти рафинированный сценарий повторения ошибки.

Поправил я эту ошибку 18 апреля 2002 года. 11-ть лет назад. Я тогда искал её ДВЕ НЕДЕЛИ. Выпуск даже затягивался. Весь мозг себе сломал. В Delphi XE3 - она до сих пор присутствует.

Вот в частности для этого и нужны автоматические тесты. Чтобы за повторяемостью таких непростых ошибок следить.

Сегодня эту ошибку под XE3 показали именно - автоматические тесты.

понедельник, 22 апреля 2013 г.

Тестировщик

Пишет коллега (то что выделено - выделено - мной):

Раньше я и не подозревал, что в нашем городе (Тверь) можно найти работу, которая непосредственно связана с тестированием каких-либо программных продуктов, приложений и т.п. Да, и вообще о существовании должности тестировщика, отделах тестирования и прочих тематических деталях мне не приходилась слышать до некоторых пор. Но как только это произошло, моё внимание было полностью скоординировано именно на этой, так сказать, карьерной ветви. Тогда я ещё учился в университете, а точнее, даже уже писал дипломный проект. А так как моё образование было целиком и полностью связано с IT-сферой, я решил, что тестирование для меня будет интересным родом деятельности, в котором я смогу стать квалифицированным специалистом.

И вот, я устроился работать тестировщиком программного обеспечения. Я был ознакомлен с программным продуктом, алгоритмами тестирования, внутренней инфраструктурой и прочими элементами, которые распахнули передо мной двери в так называемый мир Тестировщика. Задача моя была весьма понятной. Необходимо было «брать» тест-кейсы, применять их для приложения и находить, таким образом, баги. Этим я, собственно, и занимался. На самом деле, хоть это и не было довольно «крутым» делом (на мой взгляд), чувствовалось, что приобретается ценный опыт не только по данной профессии, но и в развитии аналитического мышления. И благодаря этому опыту получалось смотреть на приложения не только глазами обычного пользователя (как было в начале), но и глазами хорошего тестировщика.

Конечно, не всегда всё упиралось в тест-кейсы, так как была свобода действий (никто «не бил по рукам»), но, несмотря на это, спустя примерно полгода работа стала казаться рутинной. Тест-кейсы брались уже по n-ному кругу, а тестируемое приложение, по большому счёту, оставалось таким же (кардинальных изменений не было, а новый функционал практически не прибавлялся). Поэтому лично я чувствовал, что могу, хочу и должен делать что-то большее, приносить более весомую пользу тестируемому приложению. И вышло так, что уважаемые коллеги из Москвы предоставили мне возможность заниматься автоматизацией. Бинго! Это то, что было нужно. Новый уровень, да и вообще, другая, более серьёзная деятельность. Мне дали шанс заняться программированием. Необходимо было покрывать автоматическими тестами найденные ошибки, что считалось весьма полезным делом, ведь это:
  • обеспечивало пополнение базы автоматических тестов, которые наряду с тестировщиками, регулярно проверяли приложение на те или иные ошибки;
  • помогало программистам, которые исправляли баги, без труда прогонять тесты и проверять результаты своих исправлений;
  • позволяло отслеживать работу приложения в целом и находить такие ошибки, которые тестировщику найти было бы проблематично из-за сложного, неочевидного и/или непостоянного алгоритма повторения.
Язык, на котором требовалось писать авто-тесты, называется FORTH и представляет собой «живой язык» программирования, сгенерированный на основе Delphi. Сначала всё казалось гораздо проще, особенно, когда писался первый тест, ведь это выглядело примерно так:

: Тест
«Открыть документ»
«Выделить всё»
«Сравнить выделенный текст с эталоном»
;
Тест

Всё по-русски, всё понятно, да и работает, как надо. Но со временем всё чаще приходилось сталкиваться с ситуациями, когда содержимого словаря языка было недостаточно для того, чтобы написать тот или иной тест. И пришлось лезть ВНУТРЬ. А там ждали совсем не «Открыть документ» и «Выделить всё», там ждали уже отголоски Delphi, а также ООП. Но благодаря уважаемым коллегам и это стало более или менее перевариваться. Спустя несколько месяцев уже было в моих силах реализовать новое функционально «русское» слово, которое делало бы то, что, собственно, необходимо. Массивы, строки, деревья, стеки, рекурсия и прочие «хорошие вещи», с которыми связывала меня только студенческая жизнь, оказались очень полезными. Приходилось «играться» с размерами форм, модальными окнами, таймерами и прочим. И выходило всё достаточно неплохо, да и в удовольствие :)

 На том этапе я уже чувствовал, что то, что я делаю, можно назвать программированием, к тому же, как тестировщик, я стал смотреть на весь процесс тестирования под другим углом. Находя ту или иную ошибку, я видел набросок будущего теста, а также (в некоторых случаях) и причину бага. А самое приятное, что о рутинности и речи быть не могло. Работы было много, она была совершенно разносортной (в рамках автоматизации тестирования, конечно же), а желание «кодить» только усиливалось. А, самое приятное было тогда, когда приходилось ломать голову над одним тестом день или больше, а потом своими силами находить правильное решение. Вы ведь понимаете, о чём я? :)

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

Это была такая вводная и по большей части лирическая «зарисовка» из «кусочка» моей жизни, которая была связана с тестированием и его автоматизацией. Но так как мыслей много, а остановиться на конкретной идее в рамках данного изложения я не сумел, то хотелось бы в дальнейшем рассказать о некоторых вещах более подробно.

Владимир Селянкин.

воскресенье, 21 апреля 2013 г.

О проектном коде и тестах

В отношении к коду - я в общем-то перфекционист. Я люблю "красивый" и "ровный" код. И чтобы его можно было легко поддерживать.

В отношении к тестам у меня выработались несколько иные критерии. Я уже писал - "лучше кривые и косые, но работающие два теста, чем один (да хоть сто) идеальных, но не написанных".

Тест для начала - ДОЛЖЕН БЫТЬ. Его надо написать с минимальными потерями, чтобы он проверял, то что должен проверять.

Не стоит тратить неделю на написание "идеального теста". Это действительно замедляет движение вперёд. "Огонь и движение". Главное - наличие результата.

А вот когда тест становится трудно сопровождать - тогда я останавливаюсь и смотрю - что же с ним делать. Либо выкинуть, либо написать новый (с учётом опыта и новых обстоятельств), либо отрефакторить.

Ещё раз повторюсь - "огонь и движение". Иначе заминки дают почву для рассуждений на тему - "ах тесты усложняют жизнь и снижают производительность". А там и до рассуждений - "детализация ТЗ снижает производительность" - недалеко.

Всё вышесказанное не означает призыва "метать дурно пахнущий код". Отнюдь - нет. Просто надо всегда помнить о темпе. И нельзя позволять себе находить "отговорки" и поле для неоправданного тайм-аута типа "я сейчас пару недель подумаю и напишу супер тест". Мой опыт подсказывает, что эти "пару недель" просто будут потрачены впустую.

Попробуйте. Может быть вам понравится.

суббота, 20 апреля 2013 г.

Ссылка на учебник по Test Complete

http://tctutorial.ru/images/


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

пятница, 19 апреля 2013 г.

Ещё чуть-чуть о тестах

Некоторым читателям понравилась метафора о "контрольных точках" и разъёмах и интерфейсах, описанная тут - http://18delphi.blogspot.com/2013/04/blog-post_6244.html

Так может быть попытаться развить ещё одну метафору - о тестах - как приёмо-сдаточных испытаниях и критериях поверки? Ведь по сути они таковыми и являются. Что думаете?

вторник, 16 апреля 2013 г.

О тестах и специально оборудованных "контрольных точках"

Начну с лирического отступления.

Когда-то у меня был компьютер БК-0010.01. Купленный за бешеные 500 рублей.

Но он не подключался к моему телевизору. У телевизора не было низкочастотного входа. "Для видеомагнитофонов".

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

Нужная контрольная точка была найдена. Она находилась после каскада преобразования высокой частоты в низкую. И называлась "как сейчас помню" КТ-26.

Мы с папой припаяли к ней разьём и подключили компьютер.

И я вступил в счастливый мир "домашних компьютеров". И я смог на практике ознакомиться с архитектурой PDP-11.

Позже, освоив описанную технику, я проделал несколько раз этот "трюк" самостоятельно, без папы, с компьютерами и телевизорами моих друзей. И все были счастливы. Главное было взять схему от телевизора и найти на ней контрольную точку с нужными параметрами. Так сказать - "подобрать нужный разъём к нужному ИНТЕРФЕЙСУ".

Теперь к делу.

Когда я "всерьёз" занялся "unit"-тестированием, то я вспомнил, про эту технику и решил применить её на практике. Благо она казалась очень удобной.

Теперь постараюсь привести пример.

К сожалению - он некомпилируемый. Ибо весь код я раскрыть не могу (по названным ниже причинам - http://18delphi.blogspot.com/2013/04/disclaimer.html). Но я надеюсь, что идея станет понятной. Из того отрывка кода, который я приведу. Он - не простой. Не на три строчки. Но очень по-моему показательный.

Итак.

Вернёмся к первому моему посту - http://18delphi.blogspot.com/2013/03/blog-post.html

Там я в красках рассказывал, что мой "первый тест" в технологии DUnit был тест на проверку "предварительного просмотра печати". Это - почти правда.

Теперь давайте посмотрим как он выглядит:

Собственно тест:

unit PreviewTestBefore;

// Generated from UML model, root element: <<TestCase::Class>> Shared Delphi Operations For Tests::TestFormsTest::Everest::TPreviewTestBefore235875079
//
// Тест построения Preview

interface

uses
  afwInterfaces,
  TextEditorVisitor,
  evHAFPainterEx,
  nevShapesPaintedSpy,
  afwPreviewPageSpy,
  l3Interfaces,
  PrimTextLoad_Form,
  nevTools,
  afwPreviewPage
  ;

type
 TPreviewTest = {abstract} class(TTextEditorVisitor, IafwPreviewPanel, InevShapesLogger, IafwPagesLogger)
  {* Тест построения Preview }
 private
 // private fields
   f_Done : Boolean;
   f_Now : Cardinal;
    {* Время начала теста}
 protected
 // realized methods
   procedure SetCurrentPage(aValue: Integer);
   procedure Invalidate;
   procedure Done;
   procedure pm_SetPreviewCanvas(const aValue: IafwPreviewCanvas);
   function pm_GetPainted: Boolean;
   procedure DoVisit(aForm: TPrimTextLoadForm); override;
     {* Обработать текст }
   function OpenLog(const aView: InevView): AnsiString;
   procedure CloseLog(const aLogName: AnsiString);
   function LogScreen(const aView: InevView): Boolean;
   procedure LogPage(aPage: TafwPreviewPage;
     aCounter: Boolean);
   function ShouldStop: Boolean;
 protected
 // overridden protected methods
   procedure InitFields; override;
   function FileForOutput: AnsiString; override;
     {* Стандартный файл для вывода, для текущего теста }
   function GetNormalFontSize: Integer; override;
     {* Возвращает размер шрифта стиля "Нормальный". 0 - по-умолчанию }
   function GetFolder: AnsiString; override;
     {* Папка в которую входит тест }
   function RaiseIfEtalonCreated: Boolean; override;
 protected
 // protected fields
   f_LogNumber : Integer;
   f_CurrentOutput : AnsiString;
 protected
 // protected methods
   function PageFileName(aNumber: Integer;
     aWidthNumber: Integer;
     aCounter: Boolean;
     anEtalon: Boolean): AnsiString;
     {* Имя файла для сохранения страницы }
   procedure ReadColontituls(var theColontituls: TevColontituls); virtual;
   function GetHAFFontSize: Integer; virtual;
     {* Размер колонтитулов. 0 - по-умолчанию }
 end;//TPreviewTest

implementation

uses
  Windows,
  evStyleInterface,
  evdStyles,
  SysUtils,
  vtPreviewPanel,
  Controls,
  Forms,
  l3String,
  l3Stream,
  l3Types,
  imageenio,
  l3FileUtils,
  Graphics,
  Classes,
  KTestRunner,
  evPreviewForTestsTuning,
  l3Defaults,
  l3CanvasPrim,
  TestFrameWork
  ;

// start class TPreviewTestBefore

function TPreviewTest.PageFileName(aNumber: Integer;
  aWidthNumber: Integer;
  aCounter: Boolean;
  anEtalon: Boolean): AnsiString;
var
 l_Et : String;
 l_C  : String;
 l_WN : String;
begin
 if aCounter then
  l_C := 'C_'
 else
  l_C := '';
 if (aWidthNumber = 0) then
  l_WN := ''
 else
  l_WN := '.' + l3LeftPadChar(IntToStr(aWidthNumber), 2, '0');
 if anEtalon then
  l_Et := EtalonSuffix
 else
  l_Et := ''; 
 Result := Format('%s%s.%s%s%s%s.png',
                  [OutputPath,
                   KPage,
                   l_C,
                   l3LeftPadChar(IntToStr(aNumber), 4, '0'),
                   l_WN,
                   l_Et
                  ]);
end;//TPreviewTest.PageFileName

procedure TPreviewTest.ReadColontituls(var theColontituls: TevColontituls);
begin
 // - ничего не делаем, всё по-умолчанию
end;//TPreviewTest.ReadColontituls

function TPreviewTest.GetHAFFontSize: Integer;
begin
 Result := 0; 
end;//TPreviewTest.GetHAFFontSize

procedure TPreviewTest.SetCurrentPage(aValue: Integer);
begin
 // - ничего не делаем
end;//TPreviewTest.SetCurrentPage

procedure TPreview.Invalidate;
begin
 // - ничего не делаем
 CheckTimeout(f_Now, 120 * 60 * 1000)
 // - проверяем, что зациклились
end;//TPreviewTest.Invalidate

procedure TPreviewTest.Done;
begin
 f_Done := true;
end;//TPreviewTest.Done

procedure TPreviewTest.pm_SetPreviewCanvas(const aValue: IafwPreviewCanvas);
begin
 // - ничего не делаем
end;//TPreviewTest.pm_SetPreviewCanvas

function TPreviewTest.pm_GetPainted: Boolean;
begin
 Result := false;
end;//TPreviewTest.pm_GetPainted

procedure TPreviewTest.DoVisit(aForm: TPrimTextLoadForm);
var 
 PP : TvtPreviewPanel;
 l_OnReadColontituls : TevReadColontitulsEvent;
 l_SI : TevStyleInterface;
 l_Size : Integer;
 l_NewSize : Integer;
begin
   TafwPreviewPageSpy.Instance.SetLogger(Self);
   try
    TnevShapesPaintedSpy.Instance.SetLogger(Self);
    try
     l_SI := TevStyleInterface.Make;
     try
      l_SI.SelectStyle(ev_saHFRight);
      l_NewSize := GetHAFFontSize;
      l_Size := l_SI.Font.Size;
      try
       if (l_NewSize > 0) then
       begin
        l_SI.SelectStyle(ev_saHFLeft);
        l_SI.Font.Size := l_NewSize;
        l_SI.SelectStyle(ev_saHFRight);
        l_SI.Font.Size := l_NewSize;
       end;//l_NewSize > 0
       l_OnReadColontituls := g_OnReadColontituls;
       try
        g_OnReadColontituls := Self.ReadColontituls;
        // --------------
        // Тут идёт код, который надо раскомментировать если хочется посмотреть
        // на результаты построения preview
    (*    try
         PP := TvtPreviewPanel.Create(aForm);
         PP.Align := alClient;
         PP.Parent := aForm;
         PP.Preview := aForm.Text.Preview;
         PP.ZoomToPage(1,1,true);
        except
        end;//try..except
        repeat
         try
          Application.ProcessMessages;
         except
         end;//try..except
        until Application.Terminated;
        Exit;*)
        // --------------
        StartTimer;
        try
         f_Now := GetTickCount;
         aForm.Text.Preview.Update(Self);
        finally
         StopTimer('Preview.Update');
        end;//try..finally
       finally
        g_OnReadColontituls := l_OnReadColontituls;
       end;//try..finally
       Check(f_Done);
      finally
       l_SI.SelectStyle(ev_saHFLeft);
       l_SI.Font.Size := l_Size;
       l_SI.SelectStyle(ev_saHFRight);
       l_SI.Font.Size := l_Size;
      end;//try..finally
     finally
      FreeAndNil(l_SI);
     end;//try..finally
    finally
     TnevShapesPaintedSpy.Instance.RemoveLogger(Self);
    end;//try..finally
   finally
    TafwPreviewPageSpy.Instance.RemoveLogger(Self);
   end;//try..finally
end;//TPreviewTest.DoVisit

function TPreviewTest.OpenLog(const aView: InevView): AnsiString;
var
 l_Cnv : InevInfoCanvas;
 l_C : String;
 l_WN : String;
 l_Page : Integer;

 procedure MakeName;
 begin//MakeName
  Result := Format('%s%s.%s%s%s.shapes',
                   [OutputPath,
                    KPage,
                    l_C,
                    l3LeftPadChar(IntToStr(l_Page), 4, '0'),
                    l_WN
                   ]);
 end;//MakeName

begin
 //Inc(f_LogNumber);
 l_Cnv := aView.Metrics.InfoCanvas;
 if l_Cnv.IsPagesCounter then
  l_C := 'C_'
 else
  l_C := '';
 if (f_LogNumber = 0) then
  l_WN := ''
 else
  l_WN := '.' + l3LeftPadChar(IntToStr(f_LogNumber), 2, '0');
 f_LogNumber := l_Cnv.PageWidthNumber;
 l_Page := l_Cnv.PageNumber;
 if (l_Page > 1) then
  Dec(l_Page)
 else
  Assert(l_Page = 1);
 MakeName;
 //Result := OutputPath + KPage + '.' + l3LeftPadChar(IntToStr(f_LogNumber), 4, '0') + '.shapes';
 if (Result = f_CurrentOutput) then
 begin
  Inc(l_Page);
  MakeName;
 end;//Result = f_CurrentOutput
 f_CurrentOutput := Result;
end;//TPreviewTest.OpenLog

procedure TPreviewTest.CloseLog(const aLogName: AnsiString);
var
 l_N : String;
 l_Extra : String;
 l_Counter : Boolean;
begin
 l_N := ExtractFileName(aLogName);
 l_Counter := (Pos('.C_', l_N) > 0);
 if l_Counter then
  l_Extra := ''
 else
  l_Extra := ChangeFileExt(l_N, '.png');
 CheckOutputWithInput(ChangeFileExt(l_N, EtalonSuffix + '.shapes'),
                      #0,
                      l_Extra,
                      not l_Counter);
end;//TPreviewTest.CloseLog

function TPreviewTest.LogScreen(const aView: InevView): Boolean;
begin
 with aView.Metrics.InfoCanvas do
  Result := Printing AND not IsVirtual;
end;//TPreviewTest.LogScreen

procedure TPreviewTest.LogPage(aPage: TafwPreviewPage;
  aCounter: Boolean);
var
 l_EN  : String;
 l_N   : String;
 l_IO : TImageEnIO;
 l_B  : Graphics.TBitmap;
 l_CVSPath : String;
 l_CVS : String;
begin
 if aCounter then
  Exit;
 if (aPage.PageNumber <= 0) then
  Exit; 
 l_EN := PageFileName(aPage.PageNumber, aPage.PageWidthNumber, aCounter, true);
 l_N := PageFileName(aPage.PageNumber, aPage.PageWidthNumber, aCounter, false);
 l_IO := TImageEnIO.Create(nil);
 try
  l_B := Graphics.TBitmap.Create;
  try
   l_B.PixelFormat := pf24bit;
   l_B.Width := Trunc(IafwPreviewPage(aPage).GetMMWidth * 0.01 * 96 / 25.4);
   l_B.Height := Trunc(IafwPreviewPage(aPage).GetMMHeight * 0.01 * 96 / 25.4);
   IafwPreviewPage(aPage).DrawTo(Rect(0, 0, l_B.Width, l_B.Height), l_B);
   l_IO.Bitmap := l_B;
   l_IO.Params.BitsPerSample := 8;
   l_IO.Params.SamplesPerPixel := 1;
   l_IO.SaveToFilePNG(l_N);
  finally
   FreeAndNil(l_B);
  end;//try..finally
 finally
  FreeAndNil(l_IO);
 end;//try..finally
 if not IsWritingToK then
 begin
  if not FileExists(l_EN) then
   CopyFile(l_N, l_EN);
  if not IsFakeCVS then
  begin
   l_CVSPath := g_CVSPath + '\' + TestSetFolderName + '\';
   if DirectoryExists(l_CVSPath) then
   begin
    l_CVS := l_CVSPath + ExtractFileName(l_EN);
    if not FileExists(l_CVS) then
    begin
     CopyFile(l_N, l_CVS);
     ToLog(Format('Сделан эталон для помещения в CVS - "%s"', [l_CVS]));
    end;//not FileExists(l_CVS)
   end;//DirectoryExists(l_CVSPath)
  end;//not IsFakeCVS
 end;//not IsWritingToK
end;//TPreviewTest.LogPage

procedure TPreviewTest.InitFields;
begin
 inherited;
 f_LogNumber := 0;
 f_CurrentOutput := '';
end;//TPreviewTest.InitFields

function TPreviewTest.FileForOutput: AnsiString;
begin
 Assert(f_CurrentOutput <> '');
 Result := f_CurrentOutput;
end;//TPreviewTest.FileForOutput

function TPreviewTest.GetNormalFontSize: Integer;
begin
 Result := 12;
end;//TPreviewTest.GetNormalFontSize

function TPreviewTest.RaiseIfEtalonCreated: Boolean;
begin
 Result := false;
end;//TPreviewTest.RaiseIfEtalonCreated

end.

Тест реализует интерфейсы IafwPreviewPanel, InevShapesLogger, IafwPagesLogger и подключается к "разьёму" (контрольной точке) TnevShapesPaintedSpy и разъёму TafwPreviewPageSpy.

Интерфейсы выглядят так:
 IafwPreviewPanel = interface(IafwBase)
  {* Панель Print-preview. }
   ['{2DF654AF-58CD-4D9D-8F24-E3696D42EB3A}']
   procedure pm_SetPreviewCanvas(const aValue: IafwPreviewCanvas);
   function pm_GetPainted: Boolean;
   procedure SetCurrentPage(aValue: Integer);
   procedure Invalidate;
   procedure Done;
   property PreviewCanvas: IafwPreviewCanvas
     write pm_SetPreviewCanvas;
   property Painted: Boolean
     read pm_GetPainted;
 end;//IafwPreviewPanel

 InevShapesLogger = interface(IUnknown)
  {* Лог отрисованных объектов }
   ['{D33CDAF3-2F4B-422C-879E-56B02F0686F9}']
   function OpenLog(const aView: InevView): AnsiString;
   procedure CloseLog(const aLogName: AnsiString);
   function LogScreen(const aView: InevView): Boolean;
 end;//InevShapesLogger



 IafwPagesLogger = interface(IUnknown)
   ['{19361554-2DE2-4E58-B896-503677BDD13B}']
   procedure LogPage(aPage: TafwPreviewPage;
    aCounter: Boolean);
 end;//IafwPagesLogger


Теперь как выглядит разьём TnevShapesPaintedSpy:

unit nevShapesPaintedSpy;

// Generated from UML model, root element: <<SimpleClass::Class>> Shared Delphi::Everest::Views::TnevShapesPaintedSpy
//
// Следилка за отрисованными объектами. {RequestLink:235864309}

interface

uses
  l3Filer,
  nevTools,
  nevShapesPainted,
  l3ProtoObject
  ;

type
 InevShapesLogger = interface(IUnknown)
  {* Лог отрисованных объектов }
   ['{D33CDAF3-2F4B-422C-879E-56B02F0686F9}']
   function OpenLog(const aView: InevView): AnsiString;
   procedure CloseLog(const aLogName: AnsiString);
   function LogScreen(const aView: InevView): Boolean;
 end;//InevShapesLogger

 TnevShapesPaintedSpy = class(Tl3ProtoObject)
  {* Следилка за отрисованными объектами. [RequestLink:235864309] }
 private
 // private fields
   f_Logger : InevShapesLogger;
   f_Filer : Tl3CustomFiler;
 protected
 // overridden protected methods
   procedure Cleanup; override;
     {* Функция очистки полей объекта. }
   procedure ClearFields; override;
 public
 // public methods
   procedure LogShapes(const aView: InevView;
     aShapes: TnevBaseTopShape);
     {* Логирует отрисованные объекты }
   procedure SetLogger(const aLogger: InevShapesLogger);
   procedure RemoveLogger(const aLogger: InevShapesLogger);
   class function Exists: Boolean;
   function LogScreen(const aView: InevView): Boolean;
 public
 // singleton factory method
   class function Instance: TnevShapesPaintedSpy;
    {- возвращает экземпляр синглетона. }
 end;//TnevShapesPaintedSpy

implementation

uses
  l3Base {a},
  SysUtils,
  l3Types,
  k2Tags,
  l3String,
  evParaTools,
  nevBase,
  l3MinMax
  ;

// start class TnevShapesPaintedSpy

var g_TnevShapesPaintedSpy : TnevShapesPaintedSpy = nil;

procedure TnevShapesPaintedSpyFree;
begin
 FreeAndNil(g_TnevShapesPaintedSpy);
end;

class function TnevShapesPaintedSpy.Instance: TnevShapesPaintedSpy;
begin
 if (g_TnevShapesPaintedSpy = nil) then
 begin
  l3System.AddExitProc(TnevShapesPaintedSpyFree);
  g_TnevShapesPaintedSpy := Create;
 end;
 Result := g_TnevShapesPaintedSpy;
end;

procedure TnevShapesPaintedSpy.LogShapes(const aView: InevView;
  aShapes: TnevBaseTopShape);

 procedure LogShape(aShape : TnevShape);

  function MangleCoord(aValue : Integer): Integer;
  // - тут СОЗНАТЕЛЬНО загрубляем координаты, чтобы тесты проходили на большем числе тестовых машин

   function EpsilonIt(aValue : Integer): Integer;
   begin//EpsilonIt
    if (aValue > 0) then
     Result := Max(0, aValue - 20)
    else
    if (aValue < 0) then
     Result := Min(0, aValue + 20)
    else
     Result := aValue;
   end;//EpsilonIt

  begin//MangleCoord
   Result := (EpsilonIt(aValue) div 100) * 100;
  end;//MangleCoord

 var
  l_Index     : Integer;
  l_ImageInfo : PnevControlImageInfo;
 begin//LogShape
  if (aShape <> nil) then
  begin
   f_Filer.WriteLn('----');

   with aShape.__Obj do
   begin
    f_Filer.WriteLn(Format('Obj type = %s', [TagType.AsString]));
    if HasSubAtom(k2_tiText) then
     f_Filer.WriteLn(Format('Text = ''%s''', [l3ReplaceNonReadable(StrA[k2_tiText])]));
   end;//with aShape.__Obj
   if (aShape.Count <> 0) then
    f_Filer.WriteLn(Format('Count = %d', [aShape.Count]));
   with aShape.Bounds do
    f_Filer.WriteLn(Format('Rect = (%d, %d, %d, %d)', [MangleCoord(Top),
                                                       MangleCoord(Left),
                                                       MangleCoord(Bottom),
                                                       MangleCoord(Right)]));
   Assert(aShape.__FI <> nil);
   with aShape.__FI do
   begin
    if (Width <> 0) OR (Height <> 0) then
     f_Filer.WriteLn(Format('Dim = (%d, %d)', [MangleCoord(Width),
                                               MangleCoord(Height)]));
    if Hidden then
     f_Filer.WriteLn(Format('Hidden = %d', [Ord(Hidden)]));
    if (MaxLinesCount <> 0) then
     f_Filer.WriteLn(Format('MaxLinesCount = %d', [MaxLinesCount]));
    if (DeltaHeight <> 0) then
     f_Filer.WriteLn(Format('DeltaHeight = %d', [MangleCoord(DeltaHeight)]));
    if (Zoom <> 100) then
     f_Filer.WriteLn(Format('Zoom = %d', [Zoom]));
    if (Lines <> nil) then
     if (Lines.Count <> 1) then
      f_Filer.WriteLn(Format('LinesCount = %d', [Lines.Count]));
   end;//with aShape.__FI
   if LogScreen(aView) and evHasOwnStyle(aShape.__Obj) then
   begin
    l_ImageInfo := aShape.__FI.ImageInfo;
    if (l_ImageInfo.rFirstIndex > -1) or (l_ImageInfo.rLastIndex > -1) then
    begin
     f_Filer.WriteLn('----');
     f_Filer.WriteLn(Format('ImageInfo FirstIndex = %d, LastIndex = %d', [l_ImageInfo.rFirstIndex, l_ImageInfo.rLastIndex]));
    end; // if EvHasOwnStyle(aShape.__Obj) then
   end; // if EvHasOwnStyle(aShape.__Obj) then
   for l_Index := 0 to Pred(aShape.Count) do
    LogShape(aShape.Items[l_Index]);
  end;//aShape <> nil
 end;//LogShape

var
 l_LogName : String;
begin
 if (f_Logger <> nil) then
 begin
  l_LogName := f_Logger.OpenLog(aView);
  try
   f_Filer := Tl3CustomDOSFiler.Make(l_LogName, l3_fmWrite);
   try
    f_Filer.Open;
    try
     LogShape(aShapes);
    finally
     f_Filer.Close;
    end;//try..finally
   finally
    FreeAndNil(f_Filer);
   end;//try..finally
  finally
   f_Logger.CloseLog(l_LogName);
  end;//try..finally
 end;//f_Logger <> nil
end;//TnevShapesPaintedSpy.LogShapes

procedure TnevShapesPaintedSpy.SetLogger(const aLogger: InevShapesLogger);
begin
 Assert(f_Logger = nil);
 f_Logger := aLogger;
end;//TnevShapesPaintedSpy.SetLogger

procedure TnevShapesPaintedSpy.RemoveLogger(const aLogger: InevShapesLogger);
begin
 Assert(f_Logger = aLogger);
 f_Logger := nil;
end;//TnevShapesPaintedSpy.RemoveLogger

class function TnevShapesPaintedSpy.Exists: Boolean;
begin
 Result := (g_TnevShapesPaintedSpy <> nil);
end;//TnevShapesPaintedSpy.Exists

function TnevShapesPaintedSpy.LogScreen(const aView: InevView): Boolean;
begin
 Result := (f_Logger <> nil) AND f_Logger.LogScreen(aView);
end;//TnevShapesPaintedSpy.LogScreen

procedure TnevShapesPaintedSpy.Cleanup;
begin
 FreeAndNil(f_Filer);
 inherited;
end;//TnevShapesPaintedSpy.Cleanup

procedure TnevShapesPaintedSpy.ClearFields;
 {-}
begin
 f_Logger := nil;
 inherited;
end;//TnevShapesPaintedSpy.ClearFields

end.

Разъём TafwPreviewPageSpy выглядит так:


unit afwPreviewPageSpy;

// Generated from UML model, root element: <<SimpleClass::Class>> Shared Delphi::AFW::Draw::TafwPreviewPageSpy
//
// Следилка за TafwPreviewPage, для {RequestLink:235873282}
//

interface

uses
  afwPreviewPage,
  l3ProtoObject
  ;

type
 IafwPagesLogger = interface(IUnknown)
   ['{19361554-2DE2-4E58-B896-503677BDD13B}']
   procedure LogPage(aPage: TafwPreviewPage;
    aCounter: Boolean);
 end;//IafwPagesLogger

 TafwPreviewPageSpy = class(Tl3ProtoObject)
  {* Следилка за TafwPreviewPage, для [RequestLink:235873282] }
 private
 // private fields
   f_Logger : IafwPagesLogger;
 protected
 // overridden protected methods
   procedure ClearFields; override;
 public
 // public methods
   class function Exists: Boolean;
   procedure SetLogger(const aLogger: IafwPagesLogger);
   procedure RemoveLogger(const aLogger: IafwPagesLogger);
   procedure LogPage(aPage: TafwPreviewPage;
     aCounter: Boolean);
 public
 // singleton factory method
   class function Instance: TafwPreviewPageSpy;
    {- возвращает экземпляр синглетона. }
 end;//TafwPreviewPageSpy

implementation

uses
  l3Base {a}
  ;

// start class TafwPreviewPageSpy

var g_TafwPreviewPageSpy : TafwPreviewPageSpy = nil;

procedure TafwPreviewPageSpyFree;
begin
 FreeAndNil(g_TafwPreviewPageSpy);
end;

class function TafwPreviewPageSpy.Instance: TafwPreviewPageSpy;
begin
 if (g_TafwPreviewPageSpy = nil) then
 begin
  l3System.AddExitProc(TafwPreviewPageSpyFree);
  g_TafwPreviewPageSpy := Create;
 end;
 Result := g_TafwPreviewPageSpy;
end;

class function TafwPreviewPageSpy.Exists: Boolean;
begin
 Result := (g_TafwPreviewPageSpy <> nil);
end;//TafwPreviewPageSpy.Exists

procedure TafwPreviewPageSpy.SetLogger(const aLogger: IafwPagesLogger);
begin
 Assert(f_Logger = nil);
 f_Logger := aLogger;
end;//TafwPreviewPageSpy.SetLogger

procedure TafwPreviewPageSpy.RemoveLogger(const aLogger: IafwPagesLogger);
begin
 Assert(f_Logger = aLogger);
 f_Logger := nil;
end;//TafwPreviewPageSpy.RemoveLogger

procedure TafwPreviewPageSpy.LogPage(aPage: TafwPreviewPage;
  aCounter: Boolean);
begin
 if (f_Logger <> nil) then
  f_Logger.LogPage(aPage, aCounter);
end;//TafwPreviewPageSpy.LogPage

procedure TafwPreviewPageSpy.ClearFields;
 {-}
begin
 f_Logger := nil;
 inherited;
end;//TafwPreviewPageSpy.ClearFields

end.



Теперь где эти разъёмы вызываются:


procedure TnevShapesPainted.Clear;
 {* - Очищает список. }
begin
 try
  if (f_Shapes <> nil) then
  begin
   if (f_View <> nil) then
   begin
    if TnevShapesPaintedSpy.Exists then
     if TnevShapesPaintedSpy.Instance.LogScreen(InevView(f_View)) then
      TnevShapesPaintedSpy.Instance.LogShapes(InevView(f_View), f_Shapes);
   end;//f_View <> nil
  end;//f_Shapes <> nil
 finally
  FreeAndNil(f_Current);
  FreeAndNil(f_Shapes);
  inherited;
 end;//try..finally
end;

И:
procedure TafwPreviewPage.Drop(aCounter: Boolean);
var
 l_Stream : IStream;
 l_Index  : Integer;
begin
 if (f_MetaFile <> nil) then
 begin
  if aCounter then
   FreeAndNil(f_MetaFile)
  else
  if (f_DropStream = nil) then
  begin
   if TafwPreviewPageSpy.Exists then
    TafwPreviewPageSpy.Instance.LogPage(Self, aCounter);
   f_DropStream := TevDataCache.CreateTempStream;
   try
    l_Stream := f_DropStream.MakeForWrite;
    try
     MetaFile.SaveToIStream(l_Stream);
    finally
     l_Stream := nil;
    end;//try..finally
    FreeAndNil(f_MetaFile);
   except
    f_DropStream := nil;
    raise;
   end;//try..except
  end//f_DropStream = nil
  else
   FreeAndNil(f_MetaFile);
 end;//f_MetaFile <> nil
 for l_Index := 1 to Pred(WidthCount) do
  WidthPage(l_Index).Drop(aCounter);
end;//TafwPreviewPage.Drop

Ну в общем - как-то так. Я предупреждал - пример - непростой. Но и тестируемый код сложен. Предварительный просмотр печати и печать я отлаживал года два. И то не довёл до идеала. Но зато я теперь вижу - "завтра же" - где и что отвалилось.

Сами конечные (а не абстрактные) выглядят примерно так:
unit K182157315;

// Generated from UML model, root element: <<TestCase::Class>> Shared Delphi Tests::DailyTest::7.7::K182157315
//
// {RequestLink:182157315}
//

interface

uses
  Classes
  ,
  PreviewTest
  ;
type
 TK182157315 = class(TPreviewTest)
  {* [RequestLink:182157315] }
 end;//TK182157315

implementation

uses
  TestFrameWork
  ;

// start class TK182157315

initialization
 TestFramework.RegisterTest(TK182157315.Suite);

end.

Или так:


unit K219124975;



// Generated from UML model, root element: <<TestCase::Class>> Shared Delphi Tests::DailyTest::7.5::K219124975

//

// {RequestLink:219124975}
//

interface

uses
  Classes
  ,
  PreviewTest
  ,
  evHAFPainterEx
  ;

type
 TK219124975 = class(TPreviewTest)
  {* [RequestLink:219124975] }
 protected
 // overridden protected methods
   function TreatExceptionAsSuccess: Boolean; override;
   function GetNormalFontSize: Integer; override;
     {* Возвращает размер шрифта стиля "Нормальный". 0 - по-умолчанию }
   procedure ReadColontituls(var theColontituls: TevColontituls); override;
   function GetHAFFontSize: Integer; override;
     {* Размер колонтитулов. 0 - по-умолчанию }
   function GetFolder: AnsiString; override;
     {* Папка в которую входит тест }
   function GetModelElementGUID: AnsiString; override;
     {* Идентификатор элемента модели, который описывает тест }
 end;//TK219124975

implementation

uses
  evTypes,
  l3Base,
  TestFrameWork
  ;

// start class TK219124975

function TK219124975.TreatExceptionAsSuccess: Boolean;
begin
 Result := true;
end;//TK219124975.TreatExceptionAsSuccess

function TK219124975.GetNormalFontSize: Integer;
begin
 Result := 39;
end;//TK219124975.GetNormalFontSize

procedure TK219124975.ReadColontituls(var theColontituls: TevColontituls);
begin
 inherited;
 theColontituls[pcUpRightFirst] := l3CStr('%DocFullName%'#10'%DocRedactionDate%');
 theColontituls[pcUpRight] := theColontituls[pcUpRightFirst];
 theColontituls[pcDownRightFirst] := l3CStr(''{'%DocCurrentPage% / %DocPagesCount%'});
 theColontituls[pcDownRight] := theColontituls[pcDownRightFirst];
end;//TK219124975.ReadColontituls

function TK219124975.GetHAFFontSize: Integer;
begin
 Result := 28;
end;//TK219124975.GetHAFFontSize

initialization
 TestFramework.RegisterTest(TK219124975.Suite);

end.

А имя файла для проверки вычисляется из имени тестового класса (через ClassName и ClassType), а сам файл с таким именем лежит в тестовом репозитарии.

Вы мне скажете - "наличие контрольных точек "зашумляет" систему и она ведёт себя не так как при их отсутствии". И вы будете АБСОЛЮТНО правы. Тут бы вспомнить про принцип неопределённости Гейзенберга. Я с вами на 100% согласен. Мне даже тестировщики это говорят. Хотя им вроде бы "не полагается" знать внутреннее устройство системы. И они - ТОЖЕ ПРАВЫ. Да. На 100% эта практика не работает, но она - чертовски (!) удобна. И позволяет решать очень много задач тестирования. Это не "серебряная пуля". Это всего лишь одна из работающих практик, позволяющая решать конкретные задачи тестирования КОНКРЕТНОГО кода. Не "сферический конь". Но - работает.

Лучше - что-то, чем НИЧЕГО. Лучше кривой и косой ОДИН тест, что полное отсутствие СТА идеальных.

Попробуйте. Может быть и вам понравится.

P.S. Да ещё недостающие классы:


unit TextEditorVisitor;

// Библиотека "TestFormsTest"
// Generated from UML model, root element: <<TestCase::Class>> Shared Delphi Operations For Tests::TestFormsTest::Everest::TTextEditorVisitor
//
// Тест, работающий с текстом документа через редактор, но не изменяющий его
//

interface

uses
  TextEditorVisitorPrim
  ;

type
 TTextEditorVisitor = {abstract} class(TTextEditorVisitorPrim)
  {* Тест, работающий с текстом документа через редактор, но не изменяющий его }
 protected
 // protected methods
   function GetNormalFontSize: Integer; virtual;
     {* Возвращает размер шрифта стиля "Нормальный". 0 - по-умолчанию }
   function MaxHeight: Integer; virtual;
     {* Если возвращается не 0, то будет организован цикл подбора высоты от FormExtent.Y до MaxHeight }
 published
 // published methods
   procedure DoIt;
     {* Собственно тело теста }
 end;//TTextEditorVisitor

implementation

uses
  SysUtils,
  evStyleInterface,
  TestFrameWork
  ;

// start class TTextEditorVisitor

procedure TTextEditorVisitor.DoIt;
var
 l_SI : TevStyleInterface;
 l_Size : Integer;
 l_NewSize : Integer;
 l_MaxHeight : Integer;
begin
 l_SI := TevStyleInterface.Make;
 try
  l_NewSize := GetNormalFontSize;
  l_Size := l_SI.Font.Size;
  try
   if (l_NewSize > 0) then
    l_SI.Font.Size := l_NewSize;
   l_MaxHeight := MaxHeight;
   if (l_MaxHeight > 0) then
   begin
    f_FixedHeight := 0;
    f_FixedHeight := FormExtent.Y;
    if (f_FixedHeight < 0) then
     f_FixedHeight := 300;
    while (f_FixedHeight < l_MaxHeight) do
    begin
     try
      VisitText;
     except
      ToLog('Form height = ' + IntToStr(f_FixedHeight));
      raise;
     end;//try..except
     Inc(f_FixedHeight);
    end;//f_FixedHeight < l_MaxHeight
   end//l_MaxHeight > 0
   else
    VisitText;
  finally
   l_SI.Font.Size := l_Size;
  end;//try..finally
 finally
  FreeAndNil(l_SI);
 end;//try..finally
end;//TTextEditorVisitor.DoIt

function TTextEditorVisitor.GetNormalFontSize: Integer;
begin
 Result := 0;
end;//TTextEditorVisitor.GetNormalFontSize

function TTextEditorVisitor.MaxHeight: Integer;
begin
 Result := 0;
end;//TTextEditorVisitor.MaxHeight

end.
-----------------------------------------

unit TextEditorVisitorPrim;

//
// Библиотека "TestFormsTest"
// Generated from UML model, root element: <<TestCase::Class>> Shared Delphi Operations For Tests::TestFormsTest::Everest::TTextEditorVisitorPrim
//
// Тест, работающий с текстом документа через редактор, но не изменяющий его
//

interface

uses
  TextViaEditorProcessorPrim,
  PrimTextLoad_Form
  ;

type
 TTextEditorVisitorPrim = {abstract} class(TTextViaEditorProcessorPrim)
  {* Тест, работающий с текстом документа через редактор, но не изменяющий его }
 protected
 // protected methods
   procedure VisitText(const aStr: AnsiString = 'Load');
     {* Собственно процедура обработки текста }
   procedure DoVisit(aForm: TPrimTextLoadForm); virtual; abstract;
     {* Обработать текст }
   function TreatExceptionAsSuccess: Boolean; virtual;
 end;//TTextEditorVisitorPrim

implementation

uses
  l3Base,
  k2OperationContainer,
  TestFrameWork
  ;

// start class TTextEditorVisitorPrim

procedure TTextEditorVisitorPrim.VisitText(const aStr: AnsiString = 'Load');
var
 l_F : _FormClass_;
 l_Raise : Boolean;
 l_DisableLog : Boolean; 
begin
 l_F := MakeForm;
 try
  l_F.Show;
  l_Raise := TreatExceptionAsSuccess;
  l_DisableLog := l_Raise;
  if l_DisableLog then
   l3System.DisableExceptionToLog;
  try
   Load(l_F, KPage + '.evd', aStr);
   try
    try
     DoVisit(l_F);
    finally
     Check(not Tk2OperationContainer.CheckWasExceptionInFreeInOwner);
    end;//try..finally
   except
    if l_Raise then
     {l_Raise := false}
     Exit
     // - чтобы не пропустить ситуацию, когда тест вдруг стал проходить
    else
     raise;
   end;//try..except
  finally
   if l_DisableLog then
    l3System.EnableExceptionToLog;
  end;//try..finally
  Check(not l_Raise, 'Тест проходить не должен, т.к. текст не влезает в бумагу');
 finally
  l_F.Free;
 end;//try..finally
end;//TTextEditorVisitorPrim.VisitText

function TTextEditorVisitorPrim.TreatExceptionAsSuccess: Boolean;
begin
 Result := false;
end;//TTextEditorVisitorPrim.TreatExceptionAsSuccess

end.
--------------------------------

unit TextViaEditorProcessorPrim;

//
// Библиотека "TestFormsTest"
// Generated from UML model, root element: <<TestCase::Class>> Shared Delphi Operations For Tests::TestFormsTest::Everest::TTextViaEditorProcessorPrim
//
// Обработчик текста через редактор
//

interface

uses
  nevTools
  ,
  VCMBaseTest
  ,
  PrimTextLoad_Form,
  Types
  ;

type
 _FormClass_ = TPrimTextLoadForm;
 _FormProducer_Parent_ = TVCMBaseTest;
 {$Include FormProducer.imp.pas}
 TTextViaEditorProcessorPrim = {abstract} class(_FormProducer_)
  {* Обработчик текста через редактор }
 private
 // private fields
   f_ScrollCount : Integer;
    {* Поле для свойства ScrollCount}
 protected
 // overridden protected methods
   procedure FormMade(const aForm: _FormClass_); override;
   function MakeFormClass: FormClassRef; override;
 protected
 // protected methods
   procedure Load(aForm: TPrimTextLoadForm;
     const aFileName: AnsiString;
     const aStr: AnsiString = 'Load');
     {* Загружает документ в редактор указанной формы }
   procedure Save(aForm: TPrimTextLoadForm); virtual;
     {* Сохраняет текст из редактора в стандартный выходной файл }
   procedure Scroll(aForm: TPrimTextLoadForm;
     const aSubName: AnsiString);
   procedure ScrollBack(aForm: TPrimTextLoadForm;
     const aSubName: AnsiString);
     {* Скроллирует текст в обратном направлении }
   procedure ScrollByWeel(aForm: TPrimTextLoadForm;
     aCount: Integer;
     aGoTop: Boolean = True);
     {* Прокрутить заданное число раз с помощью колеса мыши }
   procedure ScrollByLine(aForm: TPrimTextLoadForm;
     aCount: Integer;
     aUp: Boolean;
     aFromBottom: Boolean);
     {* Прокручивать построчно заданное число раз вверх или вниз }
   procedure GotoDocumentBottom(aForm: TPrimTextLoadForm);
     {* Перейти в конец документа }
   procedure PageUp(aForm: TPrimTextLoadForm);
     {* Перейти на страницу вверх }
   procedure PageDown(aForm: TPrimTextLoadForm);
   function ScrollByPage: Boolean; virtual;
   function WebStyle: Boolean; virtual;
   function SendKey: Boolean; virtual;
     {* Управлять ли окном реактора посредсвом посылки клавиш, а не вызова методов редактора }
   procedure CheckTopAnchor(const aView: InevInputView); virtual;
     {* проверить якорь начала отрисовки после окончания прокрутки }
   function F1Like: Boolean; virtual;
   function QFLike: Boolean; virtual;
     {* Создать форму-редактор для работы с КЗ. }
   procedure DoBeforeLoad(aForm: TPrimTextLoadForm); virtual;
     {* Операции предшествующие загрузке документа }
   function AllowMultiSelect: Boolean; virtual;
     {* Разершить мультивыделение. }
   function WithBaseSearch: Boolean; virtual;
     {* Форма со строкой базового поиска. }
 public
 // public properties
   property ScrollCount: Integer
     read f_ScrollCount;
     {* Количество прокруток до конца документа }
 end;//TTextViaEditorProcessorPrim

implementation

uses
  Document_Const,
  k2OperationContainer,
  evdNativeWriter,
  l3Filer,
  SysUtils,
  evOp,
  l3InternalInterfaces,
  Forms,
  Windows,
  evCustomEditorWindow,
  Messages,
  TextLoad_Form,
  evdSchema,
  F1LikeTextLoad_Form,
  QFLikeTextLoad_Form,
  F1LikeFormWithBS_Form,
  TestFrameWork,
  vcmBase,
  l3Base
  ;

{$Include FormProducer.imp.pas}

// start class TTextViaEditorProcessorPrim

procedure TTextViaEditorProcessorPrim.Load(aForm: TPrimTextLoadForm;
  const aFileName: AnsiString;
  const aStr: AnsiString = 'Load');
begin
 with aForm do
 begin
  DoBeforeLoad(aForm);
  LoadManager.FileName := FileFromCurrent(aFileName);
  StartTimer;
  try
   LoadManager.Load(TextSource, k2_idDocument);
  finally
   StopTimer(aStr);
  end;//try..finally
 end;//with aForm
 Check(not Tk2OperationContainer.CheckWasExceptionInFreeInOwner);
end;//TTextViaEditorProcessorPrim.Load

procedure TTextViaEditorProcessorPrim.Save(aForm: TPrimTextLoadForm);
var
 l_Writer : TevdNativeWriter;
 l_Filer  : Tl3CustomFiler;
begin
 l_Writer := TevdNativeWriter.Create;
 try
  l_Writer.Binary := false;
  l_Filer := FilerForOutput;
  try
   l_Writer.Filer := l_Filer;
  finally
   FreeAndNil(l_Filer);
  end;//try..finally
  aForm.TextSource.Save(l_Writer);
 finally
  FreeAndNil(l_Writer);
 end;//try..finally
end;//TTextViaEditorProcessorPrim.Save

procedure TTextViaEditorProcessorPrim.Scroll(aForm: TPrimTextLoadForm;
  const aSubName: AnsiString);
var
 l_Now : Cardinal;
begin
 with aForm do
 begin
  if (Text.View <> nil) then
  begin
   Il3CommandTarget(Text).ProcessCommand(ev_ocTopLeft, true, 1);
   l_Now := StartTimer;
   try
    try
     f_ScrollCount := 0;
     while not Text.View.IsDocumentTailVisible do
     begin
      if ScrollByPage then
      begin
       if SendKey then
       begin
        PostMessage(Text.Handle, $100, $22, $1510001); //Зажали PgDown
        Inc(f_ScrollCount);
       end
       else
       if Il3CommandTarget(Text).ProcessCommand(ev_ocPageDown, true, 1) then
        Inc(f_ScrollCount)
       else
        break;
      end//ScrollByPage
      else
       Text.Perform(WM_VScroll, SB_WheelDown, 0);
      Application.ProcessMessages;
      CheckTimeout(l_Now, 20 * 60 * 1000);
      if ShouldStop then Break;
     end;//while not Text.View.IsDocumentTailVisible
    finally
     if ScrollByPage then
      if SendKey then
       PostMessage(Text.Handle, $101, $22, $1510001); //Отжали PgDown
    end;//try..finally
    Application.ProcessMessages;
   finally
    StopTimer('Scroll', aSubName);
   end;//try..finally
  end;//Text.View <> nil
 end;//with aForm
end;//TTextViaEditorProcessorPrim.Scroll

procedure TTextViaEditorProcessorPrim.ScrollBack(aForm: TPrimTextLoadForm;
  const aSubName: AnsiString);
var
 l_Now : Cardinal;
begin
 with aForm do
 begin
  if (Text.View <> nil) then
  begin
   GotoDocumentBottom(aForm);
   l_Now := StartTimer;
   try
    while not Text.View.TopAnchor.AtStart{.IsDocumentTailVisible} do
    begin
//     PostMessage(Text.Handle, $100, $22, $1510001); //Зажали PgDown
     if ScrollByPage then
     begin
      if not Il3CommandTarget(Text).ProcessCommand(ev_ocPageUp, true, 1) then
       break;
     end//ScrollByPage
     else
      Text.Perform(WM_VScroll, SB_WheelUp, 0);
     Application.ProcessMessages;
     CheckTimeout(l_Now, 20 * 60 * 1000);
     if ShouldStop then Break;
    end;//while not Text.View.IsDocumentTailVisible
//    PostMessage(Text.Handle, $101, $22, $1510001); //Отжали PgDown
    Application.ProcessMessages;
   finally
    StopTimer('ScrollBack', aSubName);
   end;//try..finally
  end;//Text.View <> nil
 end;//with aForm
end;//TTextViaEditorProcessorPrim.ScrollBack

procedure TTextViaEditorProcessorPrim.ScrollByWeel(aForm: TPrimTextLoadForm;
  aCount: Integer;
  aGoTop: Boolean = True);
var
 i: Integer;
begin
 with aForm do
 begin
  if aGoTop then
   Il3CommandTarget(Text).ProcessCommand(ev_ocTopLeft, true, 1);
  for i := 0 to aCount - 1 do
  begin
   Text.Perform(WM_VScroll, SB_WheelDown, 0);
   Application.ProcessMessages;
   if ShouldStop then Break;
  end;
 end; // with aForm do
end;//TTextViaEditorProcessorPrim.ScrollByWeel

procedure TTextViaEditorProcessorPrim.ScrollByLine(aForm: TPrimTextLoadForm;
  aCount: Integer;
  aUp: Boolean;
  aFromBottom: Boolean);
var
 i: Integer;
begin
 with aForm do
 begin
  if aFromBottom and aUp then
   GotoDocumentBottom(aForm);
  if aCount > 0 then
   if aUp then
    for i := 0 to aCount - 1 do
    begin
     aForm.Text.View.Scroller[True].LineUp(1);
     Application.ProcessMessages;
     if ShouldStop then Break;
    end // for i := 0 to aCount - 1 do
   else
    for i := 0 to aCount - 1 do
    begin
     aForm.Text.View.Scroller[True].LineDown(1);
     Application.ProcessMessages;
     if ShouldStop then Break;
    end // for i := 0 to aCount - 1 do
  else
   if aCount < 0 then
    if aUp then
     while not Text.View.TopAnchor.AtStart do
     begin
      aForm.Text.View.Scroller[True].LineUp(1);
      Application.ProcessMessages;
      CheckTopAnchor(aForm.Text.View);
      if ShouldStop then Break;
     end
    else
     while not Text.View.IsDocumentTailVisible do
     begin
      aForm.Text.View.Scroller[True].LineDown(1);
      Application.ProcessMessages;
      CheckTopAnchor(aForm.Text.View);
      if ShouldStop then Break;
     end;
 end; // with aForm do
end;//TTextViaEditorProcessorPrim.ScrollByLine

procedure TTextViaEditorProcessorPrim.GotoDocumentBottom(aForm: TPrimTextLoadForm);
begin
 if QFLike then
 // - иначе редактор КЗ это как-то по-своему обрабатывает
  aForm.Text.View.Caret.Bottom
 else
  Il3CommandTarget(aForm.Text).ProcessCommand(ev_ocBottomRight, true, 1);
end;//TTextViaEditorProcessorPrim.GotoDocumentBottom

procedure TTextViaEditorProcessorPrim.PageUp(aForm: TPrimTextLoadForm);
begin
 Il3CommandTarget(aForm.Text).ProcessCommand(ev_ocPageUp, true, 1);
end;//TTextViaEditorProcessorPrim.PageUp

procedure TTextViaEditorProcessorPrim.PageDown(aForm: TPrimTextLoadForm);
begin
 Il3CommandTarget(aForm.Text).ProcessCommand(ev_ocPageDown, true, 1);
end;//TTextViaEditorProcessorPrim.PageDown

function TTextViaEditorProcessorPrim.ScrollByPage: Boolean;
begin
 Result := true;
end;//TTextViaEditorProcessorPrim.ScrollByPage

function TTextViaEditorProcessorPrim.WebStyle: Boolean;
begin
 Result := true;
end;//TTextViaEditorProcessorPrim.WebStyle

function TTextViaEditorProcessorPrim.SendKey: Boolean;
begin
 Result := false;
end;//TTextViaEditorProcessorPrim.SendKey

procedure TTextViaEditorProcessorPrim.CheckTopAnchor(const aView: InevInputView);
begin
end;//TTextViaEditorProcessorPrim.CheckTopAnchor

function TTextViaEditorProcessorPrim.F1Like: Boolean;
begin
 Result := false;
end;//TTextViaEditorProcessorPrim.F1Like

function TTextViaEditorProcessorPrim.QFLike: Boolean;
begin
 Result := false;
end;//TTextViaEditorProcessorPrim.QFLike

procedure TTextViaEditorProcessorPrim.DoBeforeLoad(aForm: TPrimTextLoadForm);
begin
end;//TTextViaEditorProcessorPrim.DoBeforeLoad

function TTextViaEditorProcessorPrim.AllowMultiSelect: Boolean;
begin
 Result := True;
end;//TTextViaEditorProcessorPrim.AllowMultiSelect

function TTextViaEditorProcessorPrim.WithBaseSearch: Boolean;
begin
 Result := False;
end;//TTextViaEditorProcessorPrim.WithBaseSearch

procedure TTextViaEditorProcessorPrim.FormMade(const aForm: _FormClass_);
begin
 inherited;
 aForm.Text.WebStyle := WebStyle;
 aForm.Text.AllowMultiSelect := AllowMultiSelect;
end;//TTextViaEditorProcessorPrim.FormMade

function TTextViaEditorProcessorPrim.MakeFormClass: FormClassRef;
begin
 if WithBaseSearch then
  Result := TF1LikeFormWithBSForm
 else
 if QFLike then
  Result := TQFLikeTextLoadForm
 else
 if F1Like then
  Result := TF1LikeTextLoadForm
 else
  Result := TTextLoadForm;
end;//TTextViaEditorProcessorPrim.MakeFormClass

end.