вторник, 19 ноября 2013 г.

В думах о тестировании №5

Предыдущая серия была тут - 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 - это ВАЖНЫЕ места тестируемой системы для снятия показаний при помощи тестовой машины.

Т.е. эти места и являются "контрольными точками".

Как это тогда выглядит в коде:

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

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.

13 комментариев:

  1. Когда каждый раз открываю браузер, первым делом набираю на адресной строке "1", осталные буквы (8delphi.blogspot.com) автоматом набирается :). И читаю... читаю... голова опухает, понять пытаюсь. Вроде на дельфи, но мне не очень понятно. Это от того, что я самоучка. Меня не учили программировать. Я сам начал. Книжки все на русском были. А я не знал ни русского ни инглиш. Русский изучал по книжкам типа "Мама, папа, я и микрокалькулятор".
    Я читал и читаю блоги GunSmoker а, Багела... у них более менее понятно. А здесь нет. Но я читаю. ЧИТАЮ и коды копирую. Пытаюсь делать как ВЫ. Только-только начал понять что к чему. Мне 40 лет. И я не боюсь учится. И я УЧУСЬ.
    Читаю Ваш блог и думаю, как мне сохранить ВЕСЬ блок на диск...
    Спасибо Вам. Пишите. А я буду читать и грызить.

    ОтветитьУдалить
    Ответы
    1. "Когда *каждый раз* открываю браузер, первым делом набираю на адресной строке "1","

      Спасибо!

      "Я читал и читаю блоги GunSmoker а, Багела... у них более менее понятно."

      Ну.. Багель - это Багель, а GunSmoker - это GunSmoker. С ними тягаться - мне в пору. Они - умные.

      "Читаю Ваш блог и думаю, как мне сохранить ВЕСЬ блок на диск..."
      Ща напишу как.

      Удалить
    2. Архивы блога в XML-Atom вот тут:
      https://sourceforge.net/p/rumtmarc/code-0/HEAD/tree/trunk/Blogger/Backup/

      Удалить
  2. Статья - мегаполезная. Спасибо, Александр! Отдельная благодарность за терпение. Для скептиков поясню ценность данной публикации (с небольшим переводом с профессионального языка на обще-публицистический).

    Александр - практик. Не теоретик от псевдо-науки инженерного дела разработки ПО. Не лже-учёный, который сыпет цитатами из ненаписанных им книг. Не маскированный тролль, пытающий всем доказать их недоразвитость. Как человек, получивший классическое инженерное образование на инженерных науках, считаю статью эталонной:

    1. Не сферический код(нь) в вакууме - реальная разработка.
    2. Решаение конкретной задачи обеспечения инфраструктуры тестирования в мульти-ролевой команде.
    3. Примеры сбалансированного кода, готового к эксплуатации.
    4. Обоснование "зачем", местами даже в ущерб "почему" и "как". Время "почемучек" прошло, если мы говорим об обмене опытом между профессионалами.
    5. Нет претензий на 100% безальтернативность.

    Отдельная заслуга Александра - расписана компонентная модель на уровне понятий:
    >>Теперь определим понятия необходимые нам для тестирования:

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

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

    Очень часто в мире IT крутость измеряется степенем глубины проникновения в частные вопросы, а это заужает кругозор читающей аудитории. Профессиональный рост разработчика, однако, должен быть направлен на расширение (чуть не сказал кругозора) ПОНИМАНИЯ системы в целом изнутри вовне до уровня конечного пользователя, который есть тоже ТЕСТОВАЯ МАШИНА. Именно такое представление и делает систему массовой, знания универсальными, а функционал надёжным.

    Спасибо, Александр!
    (Ну а при чем здесь Delphi? Ну при том, что Лев Толстой должен же был выбрать какой-то язык, чтобы написать "Войну и Мир").

    ОтветитьУдалить
    Ответы
    1. Спасибо, Всеволод!
      Я ПРАВДА очень старался, чтобы было "ну уж понятней некуда".

      "5. Нет претензий на 100% безальтернативность."
      Не устаю повторять - на БЕЗАЛЬТЕРНАТИВНОСТЬ - НИГДЕ не претендую.

      Если я где-то что-то "не так" сказал - это меня просто "не так поняли" :-)

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

      Ох - "бальзам на раны". Я сам на "это" примерно так смотрю.

      Удалить
  3. Не знаю, с какого боку начать :)

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

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

    Кстати вот такой ещё момент. Нам, сегодняшним читателям повезло, что мы можем читать Ваши мысли каждый день небольшими порциями. Другие люди, которые выйдут на Ваш блог спустя пару лет, уже не будут перечитывать каждую вашу заметку. Слишком большой и неструктурированный материал. Фактически - это черновик. (У меня пару раз возникала мысль - переработать некоторые ваши заметки и подать их в другом виде.. эхъ)

    А конкретно этот пост - он отличный. Ну тут Всеволод всё написал уже. Спасибо :)

    ОтветитьУдалить
    Ответы
    1. Николай.

      Во-первых - Спасибо!

      А во-вторых - "У меня пару раз возникала мысль - переработать некоторые ваши заметки и подать их в другом виде.. эхъ"

      Так - ПЕРЕРАБОТАЙТЕ :-) Я - НЕ ПРОТИВ, я - ТОЛЬКО ЗА :-)

      Я уже кстати писал в блоге - "ищу технического писателя".

      Ибо сам силён лишь в "разговорах в курилке" :-)

      Ну и кстати предложение о личной встрече - оно в силе.

      Удалить
    2. Вы кстати неплохо "переработали" шаблоны, о которых писал я и Акжан Абдуллин (сильно раньше меня). Хотя и в неожиданном для меня свете.

      Удалить
    3. http://18delphi.blogspot.ru/2013/04/blog-post_8954.html

      Удалить
    4. Сухой остаток.

      Дальнейшие посты пишу так:
      1. Цель или мотивация.
      2. Определение терминов и понятий.
      3. Ссылка на код в репозитарии.
      4. Код.
      5. Объяснение кода.
      6. Тестовые данные.
      7. Респекты.

      Удалить
    5. "Нам, сегодняшним читателям повезло, что мы можем читать Ваши мысли каждый день небольшими порциями. "

      К сожалению я пока "умею" писать лишь в режиме "спринта".
      Есть идея - вдохнул - написал - выдохнул.

      Иначе - мысли разбегаются совсем. И вместо ОДНОЙ статьи хочется написать ДЕСЯТЬ, которые НЕ БУДУТ не написаны никогда.

      С работой кстати также - Ознакомился с ТЗ. Задал вопросы. Написал код. Ещё задал вопросы. Расставил ASSERT. Покоммител. Прогнал тесты. Исправил ошибки. Прогнал тесты. Ещё исправил ошибки. Прогнал тесты. Поставил точку.

      "Спринт".

      Хотя видимо надо для "интернет-аудитории" переходить на "стаерство".

      Удалить
    6. Когда бегаешь на 15-20 км (больше не бегал) - мозги работают совершенно в ином режиме, нежели когда на 100 м. По себе знаю.

      Удалить
  4. "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) ну или другими словами - "контрольной точки".

    Я ЗНАЮ про такую практику и САМ применяю.

    И я ХОТЕЛ рассказать и об этом, но это просто не вместилось бы в "рамки одной статьи".

    А так - тема - ХОРОШАЯ и ОБШИРНАЯ.

    И я её ещё затрону.

    ОтветитьУдалить