Предыдущая серия была тут - http://18delphi.blogspot.com/2013/11/4.html
Теперь хочется пояснить "что там написано" и зачем всё это".
На самом деле хотелось дать пример того - как тестировать проектные классы "на коленке". Не прибегая к DUnit (http://18delphi.blogspot.ru/2013/03/dunit_29.html), mock'ам (http://18delphi.blogspot.ru/2013/10/gui.html) и прочим (http://18delphi.blogspot.ru/2013/04/blog-post_8791.html) "непонятным штукам".
Я таким занимался ОЧЕНЬ ДОЛГО, пока не освоил DUnit (http://18delphi.blogspot.ru/2013/03/blog-post.html).
Как вот "просто взять проектный класс и протестировать его" (http://18delphi.blogspot.ru/2013/04/tdd-delphi.html).
Сразу оговорюсь - "серебряной пули" - не ищу. Да и "сферических коней в вакууме" - тоже.
Если кто не понял, то я говорю об УНИВЕРСАЛЬНЫХ РЕШЕНИЯХ. Их - НЕТ.
Ещё раз повторю - ИХ НЕТ. УНИВЕРСАЛЬНЫХ РЕШЕНИЙ.
Если кто-то начнёт сразу искать "недостатки подхода" и говорить, что "это всё частные случаи", то отмечу - ДА ЭТО "частные случаи".
Из ЧАСТНЫХ СЛУЧАЕВ складывается ВСЁ ТЕСТИРОВАНИЕ.
Какие-то частные случаи выводятся из требований к системе, какие-то из спецификаций к библиотекам, какие-то из уже найденных ошибок.
И эти частные случаи - ТАК или ИНАЧЕ покрывают тестируемую систему.
Абсолюта - НЕТ!
Так же как и "серебряной пули".
Но. "ЛЮБОЙ кривой и косой и полу-работающий, но НАПИСАННЫЙ тест - ЛУЧШЕ, чем миллион ИДЕАЛЬНЫХ но НЕНАПИСАННЫХ".
Хочу чтобы вы это поняли.
Если и после всего написанного - не поняли - значит ДЛЯ ВАС - время для тестов всё ещё не пришло.
Вы наверное и так - "спите спокойно". И ошибки в коде вас не тревожат.
Продолжу.
ЛЮБОЙ тест (ну о которых говорю я) - КАК МИНИМУМ позволяет вам измерять "регресс" (http://ru.wikipedia.org/wiki/%D0%A0%D0%B5%D0%B3%D1%80%D0%B5%D1%81%D1%81%D0%B8%D0%BE%D0%BD%D0%BD%D0%BE%D0%B5_%D1%82%D0%B5%D1%81%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5), т.е. - контроллировать тот факт, что в разрабатываемой системе при внесении изменений в её код - НИЧЕГО (уже оттестированное) не сломалось.
Итак.
Постараюсь разобрать это всё на примере.
Для начала сделаю ОДНО ДОПУЩЕНИЕ.
Будем разбирать тестирование того кода, который ДЕТЕРМИНИРОВАН (http://ru.wikipedia.org/wiki/%D0%94%D0%B5%D1%82%D0%B5%D1%80%D0%BC%D0%B8%D0%BD%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D1%81%D1%82%D1%8C) относительно своих "обобщённых параметров" (куда входит и скажем Self).
Теперь определим термины, с которыми мы дальше будем работать:
Конкретный объект, который мы будем тестировать - TscriptParser.
Пусть его спецификация такова:
TscriptParser - это инструмент для разбора входного потока символов. Который должен обладать следующими свойствами:
1. Он разбирает входной поток на "токены" (последовательность "не пустых символов").
2. "Токен" не может перетекать "с одной строки на другую". Признаком "конца строки" служат символы с кодами 13 и 10 или их комбинация.
3. Пустые символы это "пробел", "табуляция" и (исходя из предыдущего пункта) - символы с кодами 13 и 10.
4. Отдельно выделяется "класс" токенов - "строки обрамлённые одинарными кавычками". Признак этих токенов - "одинарная кавычка в начале", и "одинарная кавычка в конце".
5. Про "двойные кавычки" и "сдвоенные одинарные кавычки" - пусть наше ТЗ умалчивает.
6. Есть "комментарии" - ЛЮБОЙ текст строки файла который начинается с подстроки "//" и до конца текущей строки - должен ИГНОРИРОВАТЬСЯ. И в список "токенов" попадать не должен.
7. Токен не может быть пустым. Т.е. токен должен состоять хотя бы из ОДНОГО "не пустого символа".
Собственно приведённое ТЗ и решает код TscriptParser'а, который был приведён в предыдущей серии. (Ну с точностью до кавычек).
Теперь немного о том как устроен TscriptParser:
Сразу оговорюсь - это НЕ ПРИМЕР того КАК НАДО писать Parser'ы.
Ещё раз - это НЕ ПРИМЕР того КАК НАДО писать Parser'ы.
Если бы я не был ограничен рамками "примера", то я написал бы этот код совершенно по-другому. С использованием своих библиотек, с внутренним буфером, адресной арифметикой и лямбдами при разборе на "токены".
Это просто к тому, что не стоит "излишне цепляться" к самому коду парсера.
Теперь как он устроен.
Он устроен из "нескольких слоёв":
- GetChar - читает очередной символ из входного потока и сигнализирует о конце файла, если он достигнут.
- ReadLn - опирается на GetChar. Читает текущую строку входного потока от текущего места до конца файла или "признака конца строки". Он же осуществляет "препроцессинг" считанной строки на предмет наличия "строчных комментариев" и их удаления.
- NextToken - опирается на ReadLn и осуществляет разбор текущей строки на токены в соответствии с описанными выше правилами.
Теперь к тестированию.
Раньше я писал о понятии "контрольных точек" - http://18delphi.blogspot.ru/2013/04/blog-post_6244.html
Это понятие мы ещё рассмотрим.
Теперь определим понятия необходимые нам для тестирования:
1. Тестовая машина (TtestEngine) - "прибор" для измерения состояния тестируемой системы.
2. Тестируемая система - это тот код, который мы тестируем при помощи тестовой машины (без тавтологии к сожалению не обошлось). В нашем случае тестируемая система это наш класс TscriptParser
3. Эксперимент или тест (TtestInstance) - это конкретное испытание тестируемой системы для ФИКСИРОВАННЫХ входных параметров. Сделаем допущение, что эксперименты не могут быть вложенными. Т.е. пусть вся наша описываемая совокупность кода-тестов умеет проводить на отрезке времени [t1..t2] - лишь ОДИН эксперимент.
4. "Контрольная точка" или "порт подключения" (TtestSocket) - ИМЕНОВАННЫЙ (идентифицируемый, т.е. отличаемый от других таких же) объект описывающий конкретный тестируемый участок кода тестируемой системы.
5. "Измеряемое значение" (TtestValue) конкретное значение снимаемое с "порта подключения" в данный момент времени.
6. Метрика порта (TtestSocketMetric) - набор "измеряемых значений" (TtestValue и TtestMetricValues) снимаемых с КОНКРЕТНОГО порта в течении ВСЕГО эксперимента.
Пусть мы считаем, что описанные выше методы класса TscriptParser - GetChar, ReadLn и NextToken - это ВАЖНЫЕ места тестируемой системы для снятия показаний при помощи тестовой машины.
Т.е. эти места и являются "контрольными точками".
Как это тогда выглядит в коде:
Тестовая машина и связанные с ней понятия выглядят так:
Сам код парсера и вызовы тестовой машины выглядят вот так:
Ну и вызов парсера в тестовой обвязке выглядит так:
Весь код примера находится тут - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/GUITests/Chapter0/
Собственно тестовый "скрипт" для парсера - FirstScript.script выглядит так:
P.S. АНТАГОНИСТАМ Delphi - хочу сказать - "РЕБЯТА не про Delphi тут речь". Вообще говоря.
Хотите напишу ТО ЖЕ самое на Obj-C или Python?
P.P.S. Этот пост это ИМХО - ЛУЧШЕЕ, что я когда-нибудь писал "о работе". Если и он "неинтересен" или "непонятен" - то наверное стоит заканчивать с "писательством". Не могу я видимо ДОНЕСТИ то что "хотелось бы донести". Гнаться за Багелем или GunSmoker'ом - я НЕ В СИЛАХ. Я о ДРУГОМ пишу. О ПОДХОДЕ к программированию... Если - НЕПОНЯТНО, то - очень жаль.
P.P.P.S. Бьюсь об заклад.. Если бы что-то "из моих постов" написал бы "какой-нибудь" Фаулер или Ларман, то все бы сказали бы - "о чудо! нам открылась истина..."
Багелю и Gunsmoker'у - конечно - "отдельный респект и уважение".
P.P.P.P.S. Тут один хороший человек подсказал, что эта тема СИЛЬНО связана с Dependency Injection (http://ru.wikipedia.org/wiki/Dependency_Injection) и ссылку дал - http://docs.jboss.org/weld/reference/1.0.0/en-US/html/
Я эту тему ОБЯЗАТЕЛЬНО постараюсь раскрыть.
P.P.P.P.P.S. Dependency Injection кстати проще всего делать в таких языках как Objective-C или Python. Где - "по сути" нету приватных методов (все методы так или иначе доступны по имени) и где можно "переопределить метод ЭКЗЕМПЛЯРУ класса", а не всему классу в целом.
P.P.P.P.P.P.S. Пример был собран и протестирован под Delphi XE4 и Delphi XE5.
Теперь хочется пояснить "что там написано" и зачем всё это".
На самом деле хотелось дать пример того - как тестировать проектные классы "на коленке". Не прибегая к DUnit (http://18delphi.blogspot.ru/2013/03/dunit_29.html), mock'ам (http://18delphi.blogspot.ru/2013/10/gui.html) и прочим (http://18delphi.blogspot.ru/2013/04/blog-post_8791.html) "непонятным штукам".
Я таким занимался ОЧЕНЬ ДОЛГО, пока не освоил DUnit (http://18delphi.blogspot.ru/2013/03/blog-post.html).
Как вот "просто взять проектный класс и протестировать его" (http://18delphi.blogspot.ru/2013/04/tdd-delphi.html).
Сразу оговорюсь - "серебряной пули" - не ищу. Да и "сферических коней в вакууме" - тоже.
Если кто не понял, то я говорю об УНИВЕРСАЛЬНЫХ РЕШЕНИЯХ. Их - НЕТ.
Ещё раз повторю - ИХ НЕТ. УНИВЕРСАЛЬНЫХ РЕШЕНИЙ.
Если кто-то начнёт сразу искать "недостатки подхода" и говорить, что "это всё частные случаи", то отмечу - ДА ЭТО "частные случаи".
Из ЧАСТНЫХ СЛУЧАЕВ складывается ВСЁ ТЕСТИРОВАНИЕ.
Какие-то частные случаи выводятся из требований к системе, какие-то из спецификаций к библиотекам, какие-то из уже найденных ошибок.
И эти частные случаи - ТАК или ИНАЧЕ покрывают тестируемую систему.
Абсолюта - НЕТ!
Так же как и "серебряной пули".
Но. "ЛЮБОЙ кривой и косой и полу-работающий, но НАПИСАННЫЙ тест - ЛУЧШЕ, чем миллион ИДЕАЛЬНЫХ но НЕНАПИСАННЫХ".
Хочу чтобы вы это поняли.
Если и после всего написанного - не поняли - значит ДЛЯ ВАС - время для тестов всё ещё не пришло.
Вы наверное и так - "спите спокойно". И ошибки в коде вас не тревожат.
Продолжу.
ЛЮБОЙ тест (ну о которых говорю я) - КАК МИНИМУМ позволяет вам измерять "регресс" (http://ru.wikipedia.org/wiki/%D0%A0%D0%B5%D0%B3%D1%80%D0%B5%D1%81%D1%81%D0%B8%D0%BE%D0%BD%D0%BD%D0%BE%D0%B5_%D1%82%D0%B5%D1%81%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5), т.е. - контроллировать тот факт, что в разрабатываемой системе при внесении изменений в её код - НИЧЕГО (уже оттестированное) не сломалось.
Итак.
Постараюсь разобрать это всё на примере.
Для начала сделаю ОДНО ДОПУЩЕНИЕ.
Будем разбирать тестирование того кода, который ДЕТЕРМИНИРОВАН (http://ru.wikipedia.org/wiki/%D0%94%D0%B5%D1%82%D0%B5%D1%80%D0%BC%D0%B8%D0%BD%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D1%81%D1%82%D1%8C) относительно своих "обобщённых параметров" (куда входит и скажем Self).
Теперь определим термины, с которыми мы дальше будем работать:
Конкретный объект, который мы будем тестировать - TscriptParser.
Пусть его спецификация такова:
TscriptParser - это инструмент для разбора входного потока символов. Который должен обладать следующими свойствами:
1. Он разбирает входной поток на "токены" (последовательность "не пустых символов").
2. "Токен" не может перетекать "с одной строки на другую". Признаком "конца строки" служат символы с кодами 13 и 10 или их комбинация.
3. Пустые символы это "пробел", "табуляция" и (исходя из предыдущего пункта) - символы с кодами 13 и 10.
4. Отдельно выделяется "класс" токенов - "строки обрамлённые одинарными кавычками". Признак этих токенов - "одинарная кавычка в начале", и "одинарная кавычка в конце".
5. Про "двойные кавычки" и "сдвоенные одинарные кавычки" - пусть наше ТЗ умалчивает.
6. Есть "комментарии" - ЛЮБОЙ текст строки файла который начинается с подстроки "//" и до конца текущей строки - должен ИГНОРИРОВАТЬСЯ. И в список "токенов" попадать не должен.
7. Токен не может быть пустым. Т.е. токен должен состоять хотя бы из ОДНОГО "не пустого символа".
Собственно приведённое ТЗ и решает код TscriptParser'а, который был приведён в предыдущей серии. (Ну с точностью до кавычек).
Теперь немного о том как устроен TscriptParser:
Сразу оговорюсь - это НЕ ПРИМЕР того КАК НАДО писать Parser'ы.
Ещё раз - это НЕ ПРИМЕР того КАК НАДО писать Parser'ы.
Если бы я не был ограничен рамками "примера", то я написал бы этот код совершенно по-другому. С использованием своих библиотек, с внутренним буфером, адресной арифметикой и лямбдами при разборе на "токены".
Это просто к тому, что не стоит "излишне цепляться" к самому коду парсера.
Теперь как он устроен.
Он устроен из "нескольких слоёв":
- GetChar - читает очередной символ из входного потока и сигнализирует о конце файла, если он достигнут.
- ReadLn - опирается на GetChar. Читает текущую строку входного потока от текущего места до конца файла или "признака конца строки". Он же осуществляет "препроцессинг" считанной строки на предмет наличия "строчных комментариев" и их удаления.
- NextToken - опирается на ReadLn и осуществляет разбор текущей строки на токены в соответствии с описанными выше правилами.
Теперь к тестированию.
Раньше я писал о понятии "контрольных точек" - http://18delphi.blogspot.ru/2013/04/blog-post_6244.html
Это понятие мы ещё рассмотрим.
Теперь определим понятия необходимые нам для тестирования:
1. Тестовая машина (TtestEngine) - "прибор" для измерения состояния тестируемой системы.
2. Тестируемая система - это тот код, который мы тестируем при помощи тестовой машины (без тавтологии к сожалению не обошлось). В нашем случае тестируемая система это наш класс TscriptParser
3. Эксперимент или тест (TtestInstance) - это конкретное испытание тестируемой системы для ФИКСИРОВАННЫХ входных параметров. Сделаем допущение, что эксперименты не могут быть вложенными. Т.е. пусть вся наша описываемая совокупность кода-тестов умеет проводить на отрезке времени [t1..t2] - лишь ОДИН эксперимент.
4. "Контрольная точка" или "порт подключения" (TtestSocket) - ИМЕНОВАННЫЙ (идентифицируемый, т.е. отличаемый от других таких же) объект описывающий конкретный тестируемый участок кода тестируемой системы.
5. "Измеряемое значение" (TtestValue) конкретное значение снимаемое с "порта подключения" в данный момент времени.
6. Метрика порта (TtestSocketMetric) - набор "измеряемых значений" (TtestValue и TtestMetricValues) снимаемых с КОНКРЕТНОГО порта в течении ВСЕГО эксперимента.
Пусть мы считаем, что описанные выше методы класса TscriptParser - GetChar, ReadLn и NextToken - это ВАЖНЫЕ места тестируемой системы для снятия показаний при помощи тестовой машины.
Т.е. эти места и являются "контрольными точками".
Как это тогда выглядит в коде:
Тестовая машина и связанные с ней понятия выглядят так:
unit Testing.Engine; interface {$IfNDef NoTesting} uses System.Classes, Core.Obj ; type TtestSocket = record {* - контрольная точка } private rClass: TClass; rMethod: String; public constructor Create(anObject: TObject; const aMethod: String); {* - Создаёт экземпляр контрольной точки. } function EQ(const anOther: TtestSocket): Boolean; {* - Определяет тот факт, что контрольные точки - одтинаковые } end;//TtestSocket TtestValueType = (test_vtChar, test_vtString); {* - Возможные типы измеряемых значений } TtestValue = record {* - Измеряемое значение } private rType : TtestValueType; {* - Тип конкретного измеряемого значения. } rAsString : AnsiString; rAsChar : AnsiChar; public constructor CreateAsChar(aChar: AnsiChar); constructor CreateAsString(const aString: AnsiString); end;//TtestValue TtestMetricValues = record {* - Набор значений для метрики. } private f_Stream : TStream; {* Собственно место куда записываются значения метрики. } public procedure Init(const aTestName: String; const aSocket: TtestSocket); {* - Инициализирует набор значений для метрики } procedure FlushAndClear; {* - Записывает набор на постоянный носитель и освобождает его. } procedure PutValue(const aValue: TtestValue); {* - Записывает текущее значение в набор значений для метрики } end;//TtestMetricValues TtestMetric = record {* - Измеряемая метрика. } private rSocket : TtestSocket; {* - контрольная точка в которой снимается метрика } rValues : TtestMetricValues; {* - текущий набор значений, который снят с контрольной точки } public constructor Create(const aTestName: String; const aSocket: TtestSocket); function EQ(const anOther: TtestSocket): Boolean; {* - Проверяет тот факт, что метрика снимается с данной контрольной точки } procedure FlushAndClear; {* - Записывает метрику на постоянный носитель и освобождает её. } procedure PutValue(const aValue: TtestValue); overload; {* - Записывает текущее значение в набор значений для метрики } procedure PutValue(const aValue: String); overload; {* - Записывает текущее значение в набор значений для метрики } procedure PutValue(const aValue: AnsiChar); overload; {* - Записывает текущее значение в набор значений для метрики } end;//TtestMetric PtestMetric = ^TtestMetric; TtestMetrics = array of TtestMetric; {* - ВСЕ метрики снимаемые в КОНКРЕТНОМ эксперименте } TtestInstance = class(TCoreObject) {* - Эксперимент } private f_TestName : String; f_Metrics : TtestMetrics; protected procedure Cleanup; override; public constructor Create(const aTestName: String); function SocketMetric(const aSocket: TtestSocket): PtestMetric; {* - Возвращает экземпляр текущей метрики для данной "контрольной точки", для дальнеёшей работы с ней } end;//TtestInstance TtestEngine = class {* - Прибор для тестирования } public class function StartTest(const aTestName: String): TtestInstance; {* - Начинает тест (эксперимент) } class procedure StopTest; {* - Завершает текущий эксперимент } class function CurrentTest: TtestInstance; {* - Текущий проводимый эксперимент } end; {$EndIf NoTesting} implementation {$IfNDef NoTesting} uses System.SysUtils ; var g_CurrentTest : TtestInstance = nil; constructor TtestValue.CreateAsChar(aChar: AnsiChar); begin inherited; rType := test_vtChar; rAsChar := aChar; end; constructor TtestValue.CreateAsString(const aString: AnsiString); begin inherited; rType := test_vtString; rAsString := aString; end; constructor TtestSocket.Create(anObject: TObject; const aMethod: String); begin inherited; rClass := anObject.ClassType; rMethod := aMethod; end; function TtestSocket.EQ(const anOther: TtestSocket): Boolean; {* - Определяет тот факт, что контрольные точки - одтинаковые } begin Result := (Self.rClass = anOther.rClass) AND (Self.rMethod = anOther.rMethod); end; constructor TtestMetric.Create(const aTestName: String; const aSocket: TtestSocket); begin inherited; rSocket := aSocket; rValues.Init(aTestName, aSocket); end; function TtestMetric.EQ(const anOther: TtestSocket): Boolean; {* - Проверяет тот факт, что метрика снимается с данной контрольной точки } begin Result := Self.rSocket.EQ(anOther); end; procedure TtestMetric.FlushAndClear; {* - Записывает метрику на постоянный носитель и освобождает её. } begin Self.rValues.FlushAndClear; end; procedure TtestMetric.PutValue(const aValue: TtestValue); {* - Записывает текущее значение в набор значений для метрики } begin Self.rValues.PutValue(aValue); end; procedure TtestMetric.PutValue(const aValue: String); {* - Записывает текущее значение в набор значений для метрики } begin Self.rValues.PutValue(TtestValue.CreateAsString(aValue)); end; procedure TtestMetric.PutValue(const aValue: AnsiChar); {* - Записывает текущее значение в набор значений для метрики } begin Self.rValues.PutValue(TtestValue.CreateAsChar(aValue)); end; procedure TtestMetricValues.Init(const aTestName: String; const aSocket: TtestSocket); {* - Инициализирует набор значений для метрики } begin f_Stream := TFileStream.Create(ExtractFilePath(ParamStr(0)) + '\' + ExtractFileName(aTestName) + '.' + aSocket.rClass.ClassName + '.' + aSocket.rMethod + '.out', fmCreate); end; procedure TtestMetricValues.FlushAndClear; {* - Записывает набор на постоянный носитель и освобождает его. } begin FreeAndNil(f_Stream); end; procedure TtestMetricValues.PutValue(const aValue: TtestValue); {* - Записывает текущее значение в набор значений для метрики } const cEOL : AnsiString = #13#10; begin Assert(f_Stream <> nil, 'Файл для записи значений метрики не открыт'); case aValue.rType of test_vtChar: f_Stream.Write(@aValue.rAsChar, SizeOf(aValue.rAsChar)); test_vtString: begin f_Stream.Write(@aValue.rAsString[1], Length(aValue.rAsString)); f_Stream.Write(@cEOL[1], Length(cEOL)); end;//test_vtString else Assert(false, 'Неизвестный тип значения метрики'); end; end; constructor TtestInstance.Create(const aTestName: String); begin Assert(g_CurrentTest = nil, 'Вложенные эксперименты не поддерживаются'); f_TestName := aTestName; inherited Create; g_CurrentTest := Self; end; procedure TtestInstance.Cleanup; var l_Index : Integer; begin for l_Index := Low(f_Metrics) to High(f_Metrics) do f_Metrics[l_Index].FlushAndClear; Finalize(f_Metrics); inherited; end; function TtestInstance.SocketMetric(const aSocket: TtestSocket): PtestMetric; {* - Возвращает экземпляр текущей метрики для данной "контрольной точки", для дальнеёшей работы с ней } var l_Index : Integer; begin for l_Index := Low(f_Metrics) to High(f_Metrics) do if f_Metrics[l_Index].EQ(aSocket) then begin Result := @f_Metrics[l_Index]; Exit; end;//f_Metrics[l_Index].EQ(aSocket) SetLength(f_Metrics, Succ(Length(f_Metrics))); f_Metrics[High(f_Metrics)] := TtestMetric.Create(f_TestName, aSocket); Result := @f_Metrics[High(f_Metrics)]; end; class function TtestEngine.StartTest(const aTestName: String): TtestInstance; {* - Начинает тест (эксперимент)} begin Result := TtestInstance.Create(aTestName); end; class procedure TtestEngine.StopTest; {* - Завершает текущий эксперимент } begin Assert(g_CurrentTest <> nil, 'Что-то пошло не так. Нет текущего эксперимента'); FreeAndNil(g_CurrentTest); end; class function TtestEngine.CurrentTest: TtestInstance; {* - Текущий проводимый эксперимент} begin Result := g_CurrentTest; end; {$EndIf NoTesting} end.
Сам код парсера и вызовы тестовой машины выглядят вот так:
unit Script.Parser; interface uses Classes, Core.Obj ; {$IfNDef NoTesting} {$Define TestParser} {$EndIf NoTesting} type TscriptTokenType = (script_ttUnknown, script_ttToken, script_ttString); TscriptParser = class(TCoreObject) private f_Stream : TStream; f_EOF : Boolean; f_CurrentLine : String; f_PosInCurrentLine : Integer; f_Token : String; f_TokenType : TscriptTokenType; protected procedure Cleanup; override; function ReadLn: String; protected function GetChar(out aChar: AnsiChar): Boolean; public constructor Create(const aStream : TStream); overload; constructor Create(const aFileName : String); overload; function EOF: Boolean; procedure NextToken; public property TokenString: String read f_Token; end;//TscriptParser implementation uses System.SysUtils {$IfDef TestParser} , Testing.Engine {$EndIf TestParser} ; constructor TscriptParser.Create(const aStream : TStream); begin inherited Create; f_PosInCurrentLine := 1; f_EOF := false; f_Stream := aStream; end; constructor TscriptParser.Create(const aFileName : String); var l_FileName : String; begin l_FileName := ExtractFilePath(ParamStr(0)) + '\' + aFileName; Create(TFileStream.Create(l_FileName, fmOpenRead)); end; procedure TscriptParser.Cleanup; begin FreeAndNil(f_Stream); inherited; end; function TscriptParser.GetChar(out aChar: AnsiChar): Boolean; begin if (f_Stream.Read(aChar, SizeOf(aChar)) = SizeOf(aChar)) then begin Result := true; {$IfDef TestParser} TtestEngine.CurrentTest.SocketMetric(TtestSocket.Create(Self, 'GetChar')).PutValue(aChar); // - снимаем показания с текущей контрольной точки {$EndIf TestParser} end else Result := false; end; function TscriptParser.ReadLn: String; {$IfDef TestParser} var l_Result : AnsiString; {$EndIf TestParser} var l_Char : AnsiChar; l_Line : String; l_LineCommentPos : Integer; begin {$IfDef TestParser} try {$EndIf TestParser} try l_Line := ''; while GetChar(l_Char) do begin if (l_Char = #13) then begin if GetChar(l_Char) then begin if (l_Char = #10) then begin Result := l_Line; Exit; end//l_Char = #10 else Assert(false, 'Что-то пошло не так, после символа 13 нет символа 10'); end//GetChar(l_Char) else Assert(false, 'Что-то пошло не так, после символа 13 сразу конец файла'); end;//l_Char = #13 l_Line := l_Line + l_Char; end;//while GetChar(l_Char) f_EOF := true; Result := l_Line; finally l_LineCommentPos := Pos('//', Result); if (l_LineCommentPos > 0) then begin Delete(Result, l_LineCommentPos, Length(Result) - l_LineCommentPos + 1); end;//l_LineCommentPos > 0 end;//try..finally {$IfDef TestParser} finally TtestEngine.CurrentTest.SocketMetric(TtestSocket.Create(Self, 'ReadLn')).PutValue(Result); // - снимаем показания с текущей контрольной точки end;//try..finally {$EndIf TestParser} end; procedure TscriptParser.NextToken; const cQuote = #39; cWhiteSpace = [#32,#9]; begin f_TokenType := script_ttUnknown; f_Token := ''; while true do begin if (f_PosInCurrentLine >= Length(f_CurrentLine)) then begin // - Типа текущая строка ВСЯ обработана f_CurrentLine := ''; f_PosInCurrentLine := 1; end;//f_PosInCurrentLine > Length(f_CurrentLine) while(f_CurrentLine = '') do begin f_CurrentLine := ReadLn; if (f_CurrentLine = '') then if f_EOF then Exit; end;//while(f_NextToken = '') // Тут пропускаем пустые символы: while (f_PosInCurrentLine <= Length(f_CurrentLine)) do if (f_CurrentLine[f_PosInCurrentLine] in cWhiteSpace) then Inc(f_PosInCurrentLine) else break; if (f_PosInCurrentLine <= Length(f_CurrentLine)) then break; end;//while true // Тут накапливаем НЕ пустые символы: if (f_CurrentLine[f_PosInCurrentLine] = cQuote) then begin f_TokenType := script_ttString; Inc(f_PosInCurrentLine); while (f_PosInCurrentLine <= Length(f_CurrentLine)) do if (f_CurrentLine[f_PosInCurrentLine] <> cQuote) then begin f_Token := f_Token + f_CurrentLine[f_PosInCurrentLine]; Inc(f_PosInCurrentLine); end//not (f_CurrentLine[f_PosInCurrentLine] in cWhiteSpace) else break; end//f_CurrentLine[f_PosInCurrentLine] = '' else begin f_TokenType := script_ttToken; while (f_PosInCurrentLine <= Length(f_CurrentLine)) do if (not (f_CurrentLine[f_PosInCurrentLine] in cWhiteSpace)) then begin f_Token := f_Token + f_CurrentLine[f_PosInCurrentLine]; Inc(f_PosInCurrentLine); end//not (f_CurrentLine[f_PosInCurrentLine] in cWhiteSpace) else break; end;//else {$IfDef TestParser} case f_TokenType of script_ttString: TtestEngine.CurrentTest.SocketMetric(TtestSocket.Create(Self, 'NextToken')).PutValue('Single quoted string:'); script_ttToken: // - ничего не делаем ; else Assert(false, 'Что-то пошло не так'); end;//case f_TokenType TtestEngine.CurrentTest.SocketMetric(TtestSocket.Create(Self, 'NextToken')).PutValue(f_Token); // - снимаем показания с текущей контрольной точки {$EndIf TestParser} //f_CurrentLine := ''; end; function TscriptParser.EOF: Boolean; begin Result := f_EOF AND (f_CurrentLine = ''); end; end.
Ну и вызов парсера в тестовой обвязке выглядит так:
var l_Parser : TscriptParser; begin TtestEngine.StartTest(aFileName); try l_Parser := TscriptParser.Create(aFileName); try while not l_Parser.EOF do l_Parser.NextToken; finally FreeAndNil(l_Parser); end;//try..finally finally TtestEngine.StopTest; end;//try..finally end;
Весь код примера находится тут - https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/GUITests/Chapter0/
Собственно тестовый "скрипт" для парсера - FirstScript.script выглядит так:
DoNothing // - пока ничего не делаем // - и тут тоже пока ничего не делаем DoNothing1 DoNothing2 // - и тут пока ничего не делаем, но проверяем ДВА токена DoNothing3 DoNothing4 // - и тут пока ничего не делаем, но проверяем ДВА токена с пробелом в НАЧАЛЕ DoNothing5 DoNothing6// - и тут пока ничего не делаем, но проверяем ДВА токена БЕЗ пробелов в конце DoNothing7// - пока ничего не делаем, но проверяем ОДИН токен БЕЗ пробелов в конце DoNothing8 DoNothing9 DoNothing10 // - и тут пока ничего не делаем, но проверяем ТРИ токена DoNothing11 DoNothing12 DoNothing13 DoNothing14 // - и тут пока ничего не делаем, но проверяем ЧЕТЫРЕ токена // - обрабатываем "чисто пустую строку" 'aString1'// - пытаемся обработать строку 'aString2 with spaces'// - пытаемся обработать строку с пробелами // - обрабатываем "чисто пустую строку" в конце файла
P.S. АНТАГОНИСТАМ Delphi - хочу сказать - "РЕБЯТА не про Delphi тут речь". Вообще говоря.
Хотите напишу ТО ЖЕ самое на Obj-C или Python?
P.P.S. Этот пост это ИМХО - ЛУЧШЕЕ, что я когда-нибудь писал "о работе". Если и он "неинтересен" или "непонятен" - то наверное стоит заканчивать с "писательством". Не могу я видимо ДОНЕСТИ то что "хотелось бы донести". Гнаться за Багелем или GunSmoker'ом - я НЕ В СИЛАХ. Я о ДРУГОМ пишу. О ПОДХОДЕ к программированию... Если - НЕПОНЯТНО, то - очень жаль.
P.P.P.S. Бьюсь об заклад.. Если бы что-то "из моих постов" написал бы "какой-нибудь" Фаулер или Ларман, то все бы сказали бы - "о чудо! нам открылась истина..."
Багелю и Gunsmoker'у - конечно - "отдельный респект и уважение".
P.P.P.P.S. Тут один хороший человек подсказал, что эта тема СИЛЬНО связана с Dependency Injection (http://ru.wikipedia.org/wiki/Dependency_Injection) и ссылку дал - http://docs.jboss.org/weld/reference/1.0.0/en-US/html/
Я эту тему ОБЯЗАТЕЛЬНО постараюсь раскрыть.
P.P.P.P.P.S. Dependency Injection кстати проще всего делать в таких языках как Objective-C или Python. Где - "по сути" нету приватных методов (все методы так или иначе доступны по имени) и где можно "переопределить метод ЭКЗЕМПЛЯРУ класса", а не всему классу в целом.
P.P.P.P.P.P.S. Пример был собран и протестирован под Delphi XE4 и Delphi XE5.
Когда каждый раз открываю браузер, первым делом набираю на адресной строке "1", осталные буквы (8delphi.blogspot.com) автоматом набирается :). И читаю... читаю... голова опухает, понять пытаюсь. Вроде на дельфи, но мне не очень понятно. Это от того, что я самоучка. Меня не учили программировать. Я сам начал. Книжки все на русском были. А я не знал ни русского ни инглиш. Русский изучал по книжкам типа "Мама, папа, я и микрокалькулятор".
ОтветитьУдалитьЯ читал и читаю блоги GunSmoker а, Багела... у них более менее понятно. А здесь нет. Но я читаю. ЧИТАЮ и коды копирую. Пытаюсь делать как ВЫ. Только-только начал понять что к чему. Мне 40 лет. И я не боюсь учится. И я УЧУСЬ.
Читаю Ваш блог и думаю, как мне сохранить ВЕСЬ блок на диск...
Спасибо Вам. Пишите. А я буду читать и грызить.
"Когда *каждый раз* открываю браузер, первым делом набираю на адресной строке "1","
УдалитьСпасибо!
"Я читал и читаю блоги GunSmoker а, Багела... у них более менее понятно."
Ну.. Багель - это Багель, а GunSmoker - это GunSmoker. С ними тягаться - мне в пору. Они - умные.
"Читаю Ваш блог и думаю, как мне сохранить ВЕСЬ блок на диск..."
Ща напишу как.
Архивы блога в XML-Atom вот тут:
Удалитьhttps://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/Backup/
Статья - мегаполезная. Спасибо, Александр! Отдельная благодарность за терпение. Для скептиков поясню ценность данной публикации (с небольшим переводом с профессионального языка на обще-публицистический).
ОтветитьУдалитьАлександр - практик. Не теоретик от псевдо-науки инженерного дела разработки ПО. Не лже-учёный, который сыпет цитатами из ненаписанных им книг. Не маскированный тролль, пытающий всем доказать их недоразвитость. Как человек, получивший классическое инженерное образование на инженерных науках, считаю статью эталонной:
1. Не сферический код(нь) в вакууме - реальная разработка.
2. Решаение конкретной задачи обеспечения инфраструктуры тестирования в мульти-ролевой команде.
3. Примеры сбалансированного кода, готового к эксплуатации.
4. Обоснование "зачем", местами даже в ущерб "почему" и "как". Время "почемучек" прошло, если мы говорим об обмене опытом между профессионалами.
5. Нет претензий на 100% безальтернативность.
Отдельная заслуга Александра - расписана компонентная модель на уровне понятий:
>>Теперь определим понятия необходимые нам для тестирования:
Тема крайне важная и мега-полезная. Отработка сложных технических систем - это ИСКУССТВО, повыше собственно создания таких систем. Проблематика TDD покрвает лишь сугубо формальную часть создания функционала, тогда как здесь представлен более обобщенный подход.
Скорее даже рассмотренная технология похожа не на "отработку" и "испытания", а на "живой" мониторинг организма, когда жизнеспособность "аватара" контролируется сложной измерительной системой.
Очень часто в мире IT крутость измеряется степенем глубины проникновения в частные вопросы, а это заужает кругозор читающей аудитории. Профессиональный рост разработчика, однако, должен быть направлен на расширение (чуть не сказал кругозора) ПОНИМАНИЯ системы в целом изнутри вовне до уровня конечного пользователя, который есть тоже ТЕСТОВАЯ МАШИНА. Именно такое представление и делает систему массовой, знания универсальными, а функционал надёжным.
Спасибо, Александр!
(Ну а при чем здесь Delphi? Ну при том, что Лев Толстой должен же был выбрать какой-то язык, чтобы написать "Войну и Мир").
Спасибо, Всеволод!
УдалитьЯ ПРАВДА очень старался, чтобы было "ну уж понятней некуда".
"5. Нет претензий на 100% безальтернативность."
Не устаю повторять - на БЕЗАЛЬТЕРНАТИВНОСТЬ - НИГДЕ не претендую.
Если я где-то что-то "не так" сказал - это меня просто "не так поняли" :-)
"Скорее даже рассмотренная технология похожа не на "отработку" и "испытания", а на "живой" мониторинг организма, когда жизнеспособность "аватара" контролируется сложной измерительной системой. "
Ох - "бальзам на раны". Я сам на "это" примерно так смотрю.
Не знаю, с какого боку начать :)
ОтветитьУдалитьАлександр, у Вас виден прогресс налицо. Вместо потока мыслей вырисовываются вполне себе пригодные для понимания с первого прочтения статьи. Лично мне уже не приходится перечитывать по два-три раза, чтобы въехать в тему.
При этом "стиль" уважаемых блоггеров копировать не надо. Если всё разжевать для читателя, то в голове у последнего может ничего и не остаться. Мне очень нравится одно выражение... смысл которого примерно такой: "Высечь слово на камне гораздо сложнее и дольше, чем на песке, но зато сохранится оно гораздо дольше". Вникая (иногда почти мучительно) в суть описываемых Вами вещей и практик, они остаются в памяти на долго.
Мне, как ученику, это полезнее, чем прочитал и забыл.
И самое главное, если пытаться каждый свой пост оформлять как статью, то на это уйдёт очень много времени. А время - оно... у меня есть штук 6 недооформленных заметок в своих черновиках, и чем дальше уходит время, тем меньше шансов, что я их когда-нибудь оформлю и опубликую.
Кстати вот такой ещё момент. Нам, сегодняшним читателям повезло, что мы можем читать Ваши мысли каждый день небольшими порциями. Другие люди, которые выйдут на Ваш блог спустя пару лет, уже не будут перечитывать каждую вашу заметку. Слишком большой и неструктурированный материал. Фактически - это черновик. (У меня пару раз возникала мысль - переработать некоторые ваши заметки и подать их в другом виде.. эхъ)
А конкретно этот пост - он отличный. Ну тут Всеволод всё написал уже. Спасибо :)
Николай.
УдалитьВо-первых - Спасибо!
А во-вторых - "У меня пару раз возникала мысль - переработать некоторые ваши заметки и подать их в другом виде.. эхъ"
Так - ПЕРЕРАБОТАЙТЕ :-) Я - НЕ ПРОТИВ, я - ТОЛЬКО ЗА :-)
Я уже кстати писал в блоге - "ищу технического писателя".
Ибо сам силён лишь в "разговорах в курилке" :-)
Ну и кстати предложение о личной встрече - оно в силе.
Вы кстати неплохо "переработали" шаблоны, о которых писал я и Акжан Абдуллин (сильно раньше меня). Хотя и в неожиданном для меня свете.
Удалитьhttp://18delphi.blogspot.ru/2013/04/blog-post_8954.html
УдалитьСухой остаток.
УдалитьДальнейшие посты пишу так:
1. Цель или мотивация.
2. Определение терминов и понятий.
3. Ссылка на код в репозитарии.
4. Код.
5. Объяснение кода.
6. Тестовые данные.
7. Респекты.
"Нам, сегодняшним читателям повезло, что мы можем читать Ваши мысли каждый день небольшими порциями. "
УдалитьК сожалению я пока "умею" писать лишь в режиме "спринта".
Есть идея - вдохнул - написал - выдохнул.
Иначе - мысли разбегаются совсем. И вместо ОДНОЙ статьи хочется написать ДЕСЯТЬ, которые НЕ БУДУТ не написаны никогда.
С работой кстати также - Ознакомился с ТЗ. Задал вопросы. Написал код. Ещё задал вопросы. Расставил ASSERT. Покоммител. Прогнал тесты. Исправил ошибки. Прогнал тесты. Ещё исправил ошибки. Прогнал тесты. Поставил точку.
"Спринт".
Хотя видимо надо для "интернет-аудитории" переходить на "стаерство".
Когда бегаешь на 15-20 км (больше не бегал) - мозги работают совершенно в ином режиме, нежели когда на 100 м. По себе знаю.
Удалить"P.P.P.P.S. Тут один хороший человек подсказал, что эта тема СИЛЬНО связана с Dependency Injection (http://ru.wikipedia.org/wiki/Dependency_Injection) и ссылку дал - http://docs.jboss.org/weld/reference/1.0.0/en-US/html/
ОтветитьУдалитьЯ эту тему ОБЯЗАТЕЛЬНО постараюсь раскрыть."
-- в ДАННОМ КОНКРЕТНОМ примере это можно было бы решить через наследование от TscriptParser или агрегацию его в Proxy-объект.
По ОДНОМУ для каждого тестируемого метода (GetChar, ReadLn, NextToken) ну или другими словами - "контрольной точки".
Я ЗНАЮ про такую практику и САМ применяю.
И я ХОТЕЛ рассказать и об этом, но это просто не вместилось бы в "рамки одной статьи".
А так - тема - ХОРОШАЯ и ОБШИРНАЯ.
И я её ещё затрону.