понедельник, 13 октября 2014 г.

Про пред- и пост-условия. "Другое" мнение

По мотивам - Коротко. Про тесты и пост- и пред-условия.

Вот кстати "параллельное мнение" (надеюсь, что автор не обидится на меня):

"Предусловия и Постусловия сделать - нет проблем. Но, если уж следовать
логике, то лучше бы их реализовать так:
Предусловия: (
)
Выполнить (
)
Постусловия: (
)
А вот это технически (на уровне скриптов) сейчас нереализуемо. Я,
по-началу, развлекался возможностью задавать Параметры: до Выполнить и
после. Но, оказалось, что не работает так. Пришлось эту возможность
вычеркнуть.
Но это, повторюсь, текущая проблема, которая при заточке машинки может
быть легко решена.
Теперь дополнения и замечания. Параметры создавались с основной задачей -
упростить код теста. Убрать оттуда все ненужное, чтобы это все было "за
кадром". Т.е. чтобы код теста (в Выполнить) максимально соответствовал
тексту в ошибке/задаче. Это был основной постулат. Плюс очередная попытка
упростить понимание тестов. Идея в том, что "в кишки", коих уже написано
много до поры до времени тестировщик не лезет. А поддерживает их
программист/более опытный тестировщик. Но, понятно, что в нашем случае
получилась очередная утопия (пришел "Новый незамутнённый тестировщик" и влез "в кишки", а параметры
ничем ему не помогли).
Мне не очень нравится слова постусловия и предусловия и вот почему.
Параметры задумывались с целью уменьшить/упростить код теста. Это, по возможности,
отглагольные существительные (передаю привет стандарту IDEF0), указывающие на действия, которые будут
сделаны в тесте. Их основное назначение - краткость. Как в Паскале есть
begin, а в Cи "{" на не "begin inner block" или что-то подобное. Поэтому,
вот такая запись: "Если документ уже был открыт, то закончить тест с
успехом" мне режет глаза. Кстати, вот эта тоже - "Документ из базы
{("Случайный документ")}". По мне, так лучше "Случайный документ" и все.
Еще не забываем параметры по умолчанию. Их цель - убрать необходимость
писать все параметры во всех тестах. На некоторых тестах ничего писать не
нужно. Для этого существуют параметры по умолчанию и возможность опускать слово "Параметры:".
Есть у меня идеи по связыванию пред и постусловий (частично я их уже начал реализовывать). Т.е. если вставлен "Случайный
документ", то автоматически выставляется "Закрыть документ". Если были
операции с базой, то автоматически выставляется параметр "Очистка базы".
Зачем? Чтобы "чей-нибудь склероз" не разнес все тесты нафиг. А здесь
появляются два момента. Первый,  когда нужен какой-нибудь отладочный
режим. Т.е. доводим до какого-нибудь момента и ничего не закрываем. Такой режим должен учитываться.
Но тут отдельная тема для размышлений и, пока, не об ней речь.
Второй, все равно нужен контроль используемых ресурсов.
Выставили/невыставили параметр или где-нибудь в "Выполнить" что-то с базой
сделали - должна быть на выходе проверка, что база изменилась. И вот здесь
появляется новое понятие - проверяющие слова. Если слово обнаружило, что
параметра не было, а база изменилась, то она должна также выдать
Error/Warning и базу почистить. Но тогда, если есть такие слова (они,
кстати, будут внутренним понятием, вряд ли нужно их выпускать наружу)
смысл в постусловиях, частично, теряется. Они фактически выставляют флаги,
не более того. Другое дело - проверки теста. Вот их нужно задавать в
постусловиях.
Да, "Если документ уже был открыт, то закончить тест с успехом" в текущие
параметрические тесты не вписывается. Нет там сейчас такой обработки.
Досрочное окончание теста не предполагалось. Надо бы над этим подумать...
И последнее замечание, параметры (предусловия/постусловия) - это не только замена try except end,
а ближе к препроцессорной обработке. Они как замена условной компиляции. Хотя, наверно, несколько более широкое понятие. Текущая реализация - это не более, чем временное техническое ограничение. Его пока не требуется преодолевать, т.к. не накопилось достаточной массы задач, которым требуется что-то большее."

Мнение хотя и "другое" и "ортогональное", но (на мой взгляд) не нарушает "общей картины".

Коротко. Про тесты

По мотивам - Коротко. "Почему нужны тесты"

Уезжал тут в отпуск. Оставил тесты работать. На нескольких машинах.

Тесты (что удивительно) работали все дни моего отпуска. Вполне успешно.

На одной машине правда упали (под отладчиком) сообщением "Out of memory", что конечно - неприятно, но к делу отношения не имеет.

Про "out of memory" - буду разбираться отдельно.

Ну и ещё (эффект присутствия) - как только я начал глядеть на результаты тестов - хранилище неминуемо разрушилось. С симптомами, которые я искал уже "не один месяц".

Обходя всяческие инварианты и Assert'ы.

НЕПРИЯТНО. С одной стороны.Но с другой стороны - я получил массу полезной информации.

Добавил ещё вывод в лог. Перезапустил тесты.

Буду наблюдать.

Offtopic. Коротко. Нельзя никого насильно сделать счастливым

Собственно вот - "Нельзя никого насильно сделать счастливым".

Равно как и "нельзя научить хорошим практикам, тех кто до них морально не дорос".

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

По себе - знаю.

Ну и по большому количеству "окружающих".

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

Как-то так...

Как я вижу.

Почему пишу? Да "просто так"... Просто очередной раз сегодня имел разговор "на эту тему"... С человеком, который пока ещё не понял того, что "нельзя никого сделать насильно счастливым". Увидел "себя лет 5-10 назад".

Жаль что так. Но это жизнь.

пятница, 3 октября 2014 г.

Коротко. Опять о фабриках

По мотивам:

Коротко. Про IStorage
Про "рефакторинг"
Коротко. Про контроль типов

Пусть у нас есть ресурс. Например файл, который "часто" открывается на чтение.

Ну бизнес-логика - "так устроена".

Ну как-то так:

type
 TReadFile = class(TInterfacedObject, IStream)
  public
   constructor Create(const aFileName: String);
 end;//TReadFile

...

var
 l_F : IStream;
begin
 l_F := TReadFile.Create('aFileName');
end;

И мы (вдруг) понимаем, что "открытие файла" (в главном потоке) это - "бутылочное горлышко".

(Это - надуманный пример, но он "близок к жизни")

Как можно "улучшить ситуацию"?

Ну для начала сделаем так:

type
 TReadFile = class(TInterfacedObject, IStream)
  public
   constructor Create(const aFileName: String);
   class function Make(const aFileName: String): IStream;
 end;//TReadFile

...

class function TReadFile.Make(const aFileName: String): IStream;
begin
 Result := Create(aFileName);
end;

...

var
 l_F : IStream;
begin
 l_F := TReadFile.Make('aFileName');
end;

-- что мы тут сделали?

Да пока - "ничего", просто заменили конструктор, на фабрику.

Что можно сделать дальше?

Ну например можно попробовать "собрать статистику" (потокозащищённость - оставлю "за скобками"):

type
 TReadFileInfo = record
  public
   rFileName : String;
   rOpenCount : String;
   constructor Create(const aFileName: String);
 end;//TReadFileInfo

 TReadFileInfoList = class(TList<TReadFileInfo>)
  protected
   procedure IncOpenCount(const aFileName: String);
 end;//TReadFileInfoList

 TReadFile = class(TInterfacedObject, IStream)
  private
   class var f_ReadFileInfoList : TReadFileInfoList;
  protected
   class constructor Create;
   class destructor Destroy;
  public
   constructor Create(const aFileName: String);
   class function Make(const aFileName: String): IStream;
 end;//TReadFile

...

constructor TReadFileInfo.Create(const aFileName: String);
begin
 inherited;
 rFileName := aFileName;
 rOpenCount := 0;
end;

...

procedure TReadFileInfoList.IncOpenCount(const aFileName: String);
var
 l_Index : Integer;
 l_Item : TReadFileInfo;
begin
 for l_Index := 0 to Pred(Count) do
 begin
  l_Item := Items[l_Index];
  if (l_Item.rFileName = aFileName) then
  begin
   Inc(l_Item.rOpenCount);
   Items[l_Index] := l_Item;
   Exit;
  end;//l_Item.rFileName = aFileName
  Add(TReadFileInfo.Create(aFileName));
 end;//for l_Index
end;

...

class constructor TReadFileInfoList.Create;
begin
 inherited;
 f_ReadFileInfoList := TReadFileInfoList.Create;
end;

class destructor TReadFileInfoList.Destroy;
begin
 FreeAndNil(f_ReadFileInfoList);
 inherited;
end;

...

class function TReadFile.Make(const aFileName: String): IStream;
begin
 if (CurrentTreadID = MainThreadID) then
  f_ReadFileInfoList.IncOpenCount(aFileName);
 Result := Create(aFileName);
end;

...

var
 l_F : IStream;
begin
 l_F := TReadFile.Make('aFileName');
end;

-- что мы тут сделали?

А мы тут посчитали число открытий конкретного файла в основном потоке.

Зачем?

Пойдём дальше.

Пусть у нас есть "оценка" - cTooMuchOpened.

Взятая "с потолка", точнее из "практики".

Что можно сделать?

Ну сделаем примерно так:

type
 TReadFileInfo = record
  public
   rFileName : String;
   rOpenCount : String;
   constructor Create(const aFileName: String);
 end;//TReadFileInfo

 TReadFileInfoList = class(TList<TReadFileInfo>)
  protected
   pocedure IncOpenCount(const aFileName: String; out theTooMuchIndex: Integer);
 end;//TReadFileInfoList

 TReadFile = class(TInterfacedObject, IStream)
  private
   class var f_ReadFileInfoList : TReadFileInfoList;
  protected
   class constructor Create;
   class destructor Destroy;
  public
   constructor Create(const aFileName: String);
   class function Make(const aFileName: String): IStream;
 end;//TReadFile

...

constructor TReadFileInfo.Create(const aFileName: String);
begin
 inherited;
 rFileName := aFileName;
 rOpenCount := 0;
end;

...

const
 cTooMuchOpened = 1000;

procedure TReadFileInfoList.IncOpenCount(const aFileName: String; out theTooMuchIndex: Integer);
var
 l_Index : Integer;
 l_Item : TReadFileInfo;
begin
 theTooMuchIndex := -1;
 for l_Index := 0 to Pred(Count) do
 begin
  l_Item := Items[l_Index];
  if (l_Item.rFileName = aFileName) then
  begin
   Inc(l_Item.rOpenCount);
   Items[l_Index] := l_Item;
   if (l_Item.rOpenCount >= cTooMuchOpened) then
    theTooMuchIndex := l_Index;
   Exit;
  end;//l_Item.rFileName = aFileName
  Add(TReadFileInfo.Create(aFileName));
 end;//for l_Index
end;

...

class constructor TReadFileInfoList.Create;
begin
 inherited;
 f_ReadFileInfoList := TReadFileInfoList.Create;
end;

class destructor TReadFileInfoList.Destroy;
begin
 FreeAndNil(f_ReadFileInfoList);
 inherited;
end;

...

class function TReadFile.Make(const aFileName: String): IStream;
var
 l_TooMuchIndex : Integer;
begin
 if (CurrentTreadID = MainThreadID) then
 begin
  f_ReadFileInfoList.IncOpenCount(aFileName, l_TooMuchIndex);
  if (l_TooMuchIndex >= 0) then
   SystemLog.ToLog('File "' + aFileName + '" too much opened');
 end;//CurrentTreadID = MainThreadID
 Result := Create(aFileName);
end;

...

var
 l_F : IStream;
begin
 l_F := TReadFile.Make('aFileName');
end;

-- что мы тут сделали?

А тут мы научились "понимать", что какие-то файлы "слишком много открываются".

Что будем делать дальше?

Попробуем "закешировать" эти файлы. Т.е. "объекты их реализующие".

Ну как-то так:

type
 TReadFileInfo = record
  public
   rFileName : String;
   rOpenCount : String;
   rStream: IStream;
   constructor Create(const aFileName: String);
 end;//TReadFileInfo

 TReadFileInfoList = class(TList<TReadFileInfo>)
  protected
   pocedure IncOpenCount(const aFileName: String; out theTooMuchIndex: Integer);
 end;//TReadFileInfoList

 TReadFile = class(TInterfacedObject, IStream)
  private
   class var f_ReadFileInfoList : TReadFileInfoList;
  protected
   class constructor Create;
   class destructor Destroy;
  public
   constructor Create(const aFileName: String);
   class function Make(const aFileName: String): IStream;
 end;//TReadFile

...

constructor TReadFileInfo.Create(const aFileName: String);
begin
 inherited;
 rFileName := aFileName;
 rOpenCount := 0;
 rStream := nil;
end;

...

const
 cTooMuchOpened = 1000;

procedure TReadFileInfoList.IncOpenCount(const aFileName: String; out theTooMuchIndex: Integer);
var
 l_Index : Integer;
 l_Item : TReadFileInfo;
begin
 theTooMuchIndex := -1;
 for l_Index := 0 to Pred(Count) do
 begin
  l_Item := Items[l_Index];
  if (l_Item.rFileName = aFileName) then
  begin
   Inc(l_Item.rOpenCount);
   Items[l_Index] := l_Item;
   if (l_Item.rOpenCount >= cTooMuchOpened) then
    theTooMuchIndex := l_Index;
   Exit;
  end;//l_Item.rFileName = aFileName
  Add(TReadFileInfo.Create(aFileName));
 end;//for l_Index
end;

...

class constructor TReadFileInfoList.Create;
begin
 inherited;
 f_ReadFileInfoList := TReadFileInfoList.Create;
end;

class destructor TReadFileInfoList.Destroy;
begin
 FreeAndNil(f_ReadFileInfoList);
 inherited;
end;

...

class function TReadFile.Make(const aFileName: String): IStream;
var
 l_TooMuchIndex : Integer;
begin
 if (CurrentTreadID = MainThreadID) then
 begin
  f_ReadFileInfoList.IncOpenCount(aFileName, l_TooMuchIndex);
  if (l_TooMuchIndex >= 0) then
  begin
   SystemLog.ToLog('File "' + aFileName + '" too much opened');
   l_Item := f_ReadFileInfoList.Items[l_TooMuchIndex];
   if (l_Item.rStream <> nil) then
   begin
    Result := l_Item.rStream;
    // - возвращаем ранее закешированный поток
    Result.Seek(0, STREAM_SET);
    // - перематываем поток на начало (это - ВАЖНО), не зря я говорил (выше), про МНОГОПОТОЧНОСТЬ
    Exit;
    // - выходим, т.к. уже нашли нужный файл
   end;//l_Item.rStream <> nil
  end;//l_TooMuchIndex >= 0
 end;//CurrentTreadID = MainThreadID
 Result := Create(aFileName);
 if (l_TooMuchIndex >= 0) then
 begin
  l_Item := f_ReadFileInfoList.Items[l_TooMuchIndex];
  Assert(l_Item.rStream = nil, 'Убеждаемся в очевидном');
  l_Item.rStream := Result;
  // - запоминаем НАШ объект
  f_ReadFileInfoList.Items[l_TooMuchIndex] := l_Item;
  // - сохраняем его в КЕШЕ
 end;//l_TooMuchIndex >= 0
end;

...

var
 l_F : IStream;
begin
 l_F := TReadFile.Make('aFileName');
end;

-- что мы тут сделали?

А мы тут устроили кеш файлов. Т.е. не открываем их повторно, а получаем из кеша.

Повторю - пример надуманый, но "почти из жизни".

Какие есть проблемы?

А например такие, что "просто так DeleteFile уже сложно позвать".

Почему?

А потому, что он "может быть открыт".

Ну и "смежные проблемы".

Которые можно решить примерно так:

Звать не:

 DeleteFile('aFileName');

а:

...

class procedure TReadFile.DeleteFile(const aFileName: String);
begin
 f_ReadFileInfoList.Clear;
 // - отпустили ВСЕ файлы, хотя можно и ВЫБОРОЧНО отпускать, но не хочется загромождать код
 SysUtils.DeleteFile(aFileName);
end;

...

TReadFile.DeleteFile('aFileName');

-- но это уже - "тонкости", хотя и небезпроблемные.

Ну вот собственно и всё, что я хотел ещё сказать о фабриках.

Понимаю - "слом шаблона".

Но может быть - кому-нибудь понравится.

P.S. Может быть какие-то запятые или пробелы стоя не так как хотелось бы. Но я ЧЕСТНО СТАРАЛСЯ. Хотя и писал "с листа". И код - не компилировал. Хотел лишь идею проиллюстрировать.

P.P.S. Если кто-то захочет спросить про SHARE_READ или SHARE_WRITE - скажу - "это правильный ход мыслей", но тут же процитирую одного своего педагога, который на вопрос "почему мы берём тангенс бесконечно малого, а не само бесконечно малое" ответил - "давайте на глупые вопросы Саши Люлина не будем тратить время".

четверг, 2 октября 2014 г.

Коротко. Про контроль типов

По мотивам:

Коротко. Про IStorage
Коротко. "Почему нужны тесты"

Я уже привык к тому, что выступаю в роли "капитана очевидность", но всё же - не могу не написать.

В Delphi - всё неплохо с контролем типов, но если только эти типы не атомарные.

А вот с атомарными типами - есть "шероховатости".

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

Попробую пояснить - что я имею в виду.

Пусть у нас есть объект-менеджер, который умеет распределять два типа ресурсов (в пределе - N).

Например этот объект выделяет блоки (двух разных типов) на файловой системе (потому и Int64).

И пусть он выглядит так:

type
 TmyResource = Int64;

 TmyAllocator = class
  public
   class function AllocResource1: TmyResource; 
   class function AllocResource2: TmyResource; 
   class procedure FreeResource1(var theResource: TmyResource); 
   class procedure FreeResource2(var theResource: TmyResource); 
 end;//TmyAllocator

-- в чём тут потенциальная проблема?

А в том, что можно написать так:

var
 l_Res : TmyResource;
...
begin
 l_Res := TmyAllocator.AllocResource1;
 ...
 TmyAllocator.FreeResource2(l_Res);
end;

-- Т.е. распределили ресурс одного типа, а освобождаем - ресурс другого типа.

И компилятор тут нам - "не помощник".

И про проблемы мы можем узнать лишь по "косвенным признакам". Типа AV в Run-time или в лучшем случае - Assert.

Что можно сделать?

Можно попытаться написать так:

type
 TmyResource1 = Int64;
 TmyResource2 = Int64;

 TmyAllocator = class
  public
   class function AllocResource1: TmyResource1; 
   class function AllocResource2: TmyResource2; 
   class procedure FreeResource1(var theResource: TmyResource1); 
   class procedure FreeResource2(var theResource: TmyResource2); 
 end;//TmyAllocator

var
 l_Res : TmyResource1;
...
begin
 l_Res := TmyAllocator.AllocResource1;
 ...
 TmyAllocator.FreeResource2(l_Res);
end;

-- но и тут - компилятор - нам не поможет.

Можно даже попытаться написать так:

type
 TmyResource1 = type Int64;
 TmyResource2 = type Int64;

 TmyAllocator = class
  public
   class function AllocResource1: TmyResource1; 
   class function AllocResource2: TmyResource2; 
   class procedure FreeResource1(var theResource: TmyResource1); 
   class procedure FreeResource2(var theResource: TmyResource2); 
 end;//TmyAllocator

var
 l_Res : TmyResource1;
...
begin
 l_Res := TmyAllocator.AllocResource1;
 ...
 TmyAllocator.FreeResource2(l_Res);
end;

-- но и тут - компилятор - нам не поможет.

Что делать?

"Мой ответ" - избавится от "хакерства" и перейти от атомарных типов к неатомарным.

Как?

Ну банально например вот так:

type
 TmyResource1 = record
  public
   rPosition : Int64;
   constructor Create(aPosition: Int64);
 end;//TmyResource1

 TmyResource2 = record
  public
   rPosition : Int64;
   constructor Create(aPosition: Int64);
 end;//TmyResource2

 TmyAllocator = class
  public
   class function AllocResource1: TmyResource1; 
   class function AllocResource2: TmyResource2; 
   class procedure FreeResource1(var theResource: TmyResource1); 
   class procedure FreeResource2(var theResource: TmyResource2); 
 end;//TmyAllocator
...
constructor TmyResource1.Create(aPosition: Int64);
begin
 rPosition := aPosition;
end;

constructor TmyResource2.Create(aPosition: Int64);
begin
 rPosition := aPosition;
end;
...
class function TmyAllocator.AllocResource1: TmyResource1;
begin
 Result := TmyResource1.Create(Self.AllocPos1);
end;

class function TmyAllocator.AllocResource2: TmyResource1;
begin
 Result := TmyResource2.Create(Self.AllocPos2);
end;

var
 l_Res : TmyResource1;
...
begin
 l_Res := TmyAllocator.AllocResource1;
 ...
 TmyAllocator.FreeResource2(l_Res);
 // - Вот тут компилятор - ЗАРУГАЕТСЯ
end;

- т.е. в данном случае - компилятор нам - помощник.

Почему записи, а не объекты?

Ну чтобы избежать "накладных расходов".

Хотя объекты - конечно - предпочтительнее.

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

Капитан-очевидность.

Но!

Ещё одна ремарка - пусть у нас есть даже не ресурсы, а просто "идентификаторы":

type
 TUserID = Integer;
 TGroupID = Integer;

-- как их "случайно не перепутать"?

А всё так же:

type
 TUserID = record
  public
   rID : Integer;
   constructor Create(anID: Integer);
 end;//TUserID

 TGroupID = record
  public
   rID : Integer;
   constructor Create(anID: Integer);
 end;//TGroupID

Ну вот собственно и всё.

Может быть кому-нибудь понравится.

среда, 1 октября 2014 г.

Коротко. Про тесты и пост- и пред-условия.

По мотивам - Коротко. "Почему нужны тесты".

Посмотрим на код теста:

Параметры: ( "Документ из базы {("Случайный документ")}" )
Выполнить (...)

-- тут есть ДВЕ части - Параметры и Выполнить.

Что есть что?

Параметры - это набор пост- и пред-условий.

Это не я придумал, и не я реализовал, но мне это очень нравится.

Ну а Выполнить - это собственно - код теста.

Хочу немного рассказать про пред- и пост-условия.

Что это такое?

Это инварианты, которые выполняются до и после выполнения кода теста.

И выполнение этих вариантов - гарантирует скриптовая машина.

Если инвариант, для предусловия - не выполняется, то тест даже не запускается.
Если инвариант, для постусловия - не выполняется, то последующие тесты не запускаются. (В реальности не совсем так, то мысль именно в этом)

Что написано тут:

Параметры: ( "Документ из базы {("Случайный документ")}" )

?

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

А что до постусловий?

Как они выглядят?

Да так же.

Параметры: ( 
 "Документ из базы {("Случайный документ")}" 
 "Закрывать все открытые окна"
)

Тут - "Документ из базы {("Случайный документ")}" это наше знакомое предусловие.

А вот - "Закрывать все открытые окна" - это постусловие.

Оно означает, что после выполнения теста все открытые окна приложения будут закрыты.
И это сделает не "код теста", а скриптовая машина. Независимо от того - прошёл тест или нет.

Мне не очень нравится, что предусловия и постусловия тут синтаксически неразличимы.
Т.е. предусловие это или постусловие - это зависит от реализации.

Меня лично это смущает. Но пока - так сделано. В реальности.

В будущем (наверное) стоит сделать так:

Предусловия: ( 
 "Документ из базы {("Случайный документ")}" 
 ...
 "Если документ уже был открыт, то закончить тест с успехом" // - Это полезно для много-процессных тестов, которые конкурируют за ресурс
)
Постусловия: (
 "Закрывать все открытые окна"
 ...
 "Изменяемый документ должен совпасть с эталоном" // - это крайне полезно, если мы в тесте меняем документ и хотим проконтроллировать корректность изменений и сохранения
 ...
 "Сбрасывать базу в исходное состояние"
)
Выполнить: (
 // - Тут код собственно теста
)

-- так должно бы быть, но это "пока мечты".

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

Конечно же пост- и пред-условия это некая замена конструкциям try..finally и try..except, на (на мой взгляд) - гораздо более читабельная и самодокументируемая.

Как это всё устроено?

Предусловия - это "массив функторов", которые вызываются до теста.

Ну и постусловия - это "массив функторов", которые вызываются после теста.

При этом код "кишков скриптовой машины" выглядит примерно так:

for Предусловие in Предусловия do
 try
  Если НЕ Предусловие то
   raise 'Тест не прошёл из-за предусловий'
 except
  raise 'Тест не прошёл из-за предусловий'
 end;

BOOLEAN VAR "Тест прошёл"
"Тест прошёл" := ДА
try
 try
  "Код теста"
 except
  "Тест прошёл" := НЕТ
 end
finally
 BOOLEAN VAR "Всё хорошо"
 "Всё хорошо" := ДА
 for Постусловие in Постусловия do
  try
   Если НЕ Постусловие то
    "Всё хорошо" := НЕТ
  except
   "Всё хорошо" := НЕТ
 end;
 Если НЕ "Тест прошёл" то
  raise 'Тест не прошёл сам по себе'
 Если НЕ "Всё хорошо" то
  raise 'Тест не прошёл из-за постусловий'
end

-- ну как-то так..

P.S. Вот кстати "параллельное мнение" (надеюсь, что автор не обидится на меня):

"Предусловия и Постусловия сделать - нет проблем. Но, если уж следовать
логике, то лучше бы их реализовать так:
Предусловия: (
)
Выполнить (
)
Постусловия: (
)
А вот это технически (на уровне скриптов) сейчас нереализуемо. Я,
по-началу, развлекался возможностью задавать Параметры: до Выполнить и
после. Но, оказалось, что не работает так. Пришлось эту возможность
вычеркнуть.
Но это, повторюсь, текущая проблема, которая при заточке машинки может
быть легко решена.
Теперь дополнения и замечания. Параметры создавались с основной задачей -
упростить код теста. Убрать оттуда все ненужное, чтобы это все было "за
кадром". Т.е. чтобы код теста (в Выполнить) максимально соответствовал
тексту в ошибке/задаче. Это был основной постулат. Плюс очередная попытка
упростить понимание тестов. Идея в том, что "в кишки", коих уже написано
много до поры до времени тестировщик не лезет. А поддерживает их
программист/более опытный тестировщик. Но, понятно, что в нашем случае
получилась очередная утопия (пришел "Новый незамутнённый тестировщик" и влез "в кишки", а параметры
ничем ему не помогли).
Мне не очень нравится слова постусловия и предусловия и вот почему.
Параметры задумывались с целью уменьшить/упростить код теста. Это, по возможности,
отглагольные существительные (передаю привет стандарту IDEF0), указывающие на действия, которые будут
сделаны в тесте. Их основное назначение - краткость. Как в Паскале есть
begin, а в Cи "{" на не "begin inner block" или что-то подобное. Поэтому,
вот такая запись: "Если документ уже был открыт, то закончить тест с
успехом" мне режет глаза. Кстати, вот эта тоже - "Документ из базы
{("Случайный документ")}". По мне, так лучше "Случайный документ" и все.
Еще не забываем параметры по умолчанию. Их цель - убрать необходимость
писать все параметры во всех тестах. На некоторых тестах ничего писать не
нужно. Для этого существуют параметры по умолчанию и возможность опускать слово "Параметры:".
Есть у меня идеи по связыванию пред и постусловий (частично я их уже начал реализовывать). Т.е. если вставлен "Случайный
документ", то автоматически выставляется "Закрыть документ". Если были
операции с базой, то автоматически выставляется параметр "Очистка базы".
Зачем? Чтобы "чей-нибудь склероз" не разнес все тесты нафиг. А здесь
появляются два момента. Первый,  когда нужен какой-нибудь отладочный
режим. Т.е. доводим до какого-нибудь момента и ничего не закрываем. Такой режим должен учитываться.
Но тут отдельная тема для размышлений и, пока, не об ней речь.
Второй, все равно нужен контроль используемых ресурсов.
Выставили/невыставили параметр или где-нибудь в "Выполнить" что-то с базой
сделали - должна быть на выходе проверка, что база изменилась. И вот здесь
появляется новое понятие - проверяющие слова. Если слово обнаружило, что
параметра не было, а база изменилась, то она должна также выдать
Error/Warning и базу почистить. Но тогда, если есть такие слова (они,
кстати, будут внутренним понятием, вряд ли нужно их выпускать наружу)
смысл в постусловиях, частично, теряется. Они фактически выставляют флаги,
не более того. Другое дело - проверки теста. Вот их нужно задавать в
постусловиях.
Да, "Если документ уже был открыт, то закончить тест с успехом" в текущие
параметрические тесты не вписывается. Нет там сейчас такой обработки.
Досрочное окончание теста не предполагалось. Надо бы над этим подумать...
И последнее замечание, параметры (предусловия/постусловия) - это не только замена try except end,
а ближе к препроцессорной обработке. Они как замена условной компиляции. Хотя, наверно, несколько более широкое понятие. Текущая реализация - это не более, чем временное техническое ограничение. Его пока не требуется преодолевать, т.к. не накопилось достаточной массы задач, которым требуется что-то большее."

Коротко. "Почему нужны тесты"

Сделали тут нагрузочные тесты, я писал об этом - Коротко. Сделали нагрузочные тесты.

Каков итог?

Мало того, что я добился "стабильности работы" на "ограниченном числе клиентов". Т.е. проблемы и отказы - протоколируются, но данные - не разрушатся. Максимум что случается - это "перезапуск транзакции".

Повторю - "на ограниченном количестве клиентов".

В реальных условиях - пока не тестировал. Но есть определённый оптимизм.

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

"Разрулил". Более-менее успешно. Опять же - "на ограниченном количестве клиентов".

Далее - автоматическое нагрузочное тестирование с числом клиентов "сравнимым с реальным". Стенды - готовим. Думаю - запустим и посмотрим.

Ну и ещё один вывод был сделан - "схема данных не самая удачная". Будем думать над схемой.

Она родилась лет 10-15-ть назад и видимо "уже не вписывается в современные реалии". Так бывает.

Будем думать и над схемой тоже.

Один только вопрос я "сам себе задаю" - "а что мешало сделать подобные нагрузочные тесты уже несколько лет назад". При том, что автоматические GUI-тесты и Unit-тесты - давно уже внедрены и работают. И показывают хотя бы "регресс".

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

А может быть - "не хватало инфраструктуры", которую мы наконец "набрали".

Не поленюсь опять привести код теста, ибо он - "хорош", почти по-русски:

USES
 QuickTestsUtils.script
;
 
Тест TK565491886
 
 ARRAY VAR "Список документов"
 [ Конституция ГК НК ТК ТНВЭД ОКОФ ОКОНХ ] >>> "Список документов"         
 
 BOOLEAN VAR Вечность
 ДА >>> Вечность 
 ПОКА Вечность (
          INTEGER VAR "Случайный документ"
          ( "Список документов" array:Count Random "Список документов" [i] ) >>> "Случайный документ" 
     Параметры: ( "Документ из базы {("Случайный документ")}" )
     Выполнить ( 
  "Набить текст {('Вносим изменения в текст и сохраняем документ!!!')}"
  "Нажать Enter" 
  // - чтобы отделить параграфы документа, 
  //   иначе параграфы могут быть "слишком длинными" для восприятия
  "Сохранить документ"
  "Обработать сообщения" // - чтобы приложение перерисовывалось
  Если "Нажали кнопку 'Прервать тест'" то Выходим 
  // - это ВАЖНО, потому, что наш тест - "бесконечный" 
  //   и его надо как-то прерывать (не только по Ctrl-F2)
   )
        )
;
 
TK565491886

Чего "греха таить" - я лично - "горжусь тем, что приложил руку к тому, чтобы это работало".

Причём работало "именно в таком виде".

По-русски - это конечно ерунда. Можно и по-китайски. Такая возможность - есть. Главное, что код теста - "самодокументируемый".

Но однако - "мы вышли на новый уровень тестирования". Это факт.

Есть один момент, который пока ещё "не автоматизирован". Это "автоматический деплоймент тестов" на распределённых клиентов. Но я думаю, что мы скоро и это - тоже сделаем.

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

Хотя конечно же - "дьявол кроется в деталях".

Поживём увидим.

Но вопрос - "что же мы не сделали подобные тесты ещё несколько лет назад" - я сам для себя - пока не решил.

Ну а сухой остаток - "тесты это более чем полезно". Всякие разные.

1. Unit.
2. GUI.
3. Нагрузочные.

Подробнее я писал когда-то тут - Ещё раз об "уровнях тестирования".
Ну и тут - GUI-тестирование "по-русски". Заметка об уровнях тестирования.

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

P.S. Анализ "вскрывшихся проблем" - я отдельно ещё напишу.

Там есть вопросы про дырявую абстракцию в частности.

Во-первых потому, что в ReadFile и WriteFile - "дырявая абстракция" при "отказах сети".
А во-вторых - наши собственные классы - тоже "дырявая абстракция". Как выяснилось.

P.P.S. И ещё "из части догадок". Так как "натурный эксперимент поставлен пока не был".

Но косвенные признаки говорят о чём:

Если на одном клиенте сделать так:

LockFile(hFile, 10 {anOffset - смещение региона для залочки}, 20{aLength - длина региона});

А на другом так:

SetPosition(hFile, 0 {- позиция файла});
Result := ReadFile(hFile, @aData {- куда читаем}, 10 {- сколько читаем}, @l_ReadBytes {- сколько считали});
Assert(10 = l_ReadBytes); // - проходит
Assert(Result); // - не проходит

-- то есть вероятность, что "второй клиент" получит ошибку LOCKVIOLATION.

Записи о таких ситуациях я наблюдал в логе.

Хотя регионы - вроде не пересекаются.

Потому что залоченный регион это - [10, 20].

А читаемый регион это - [0, 9].

[A, B] - это интервал в "смысле математики", во включением концов.

Т.е. - "есть впечатление", что "иногда" при чтении/записи если конец читаемого/записываемого интервала "упирается" в залоченный интервал, то функции ReadFile/WriteFile - могут возвращать ошибку.

P.P.P.S. Кстати можно посмотреть на "код тестов".

P.P.P.P.S. Ну и напомню - это.