Delphi unit test для TThread с FreeOnTerminate = True

Каков наилучший способ написать тест Delphi DUnit для потомка TThread, когда FreeOnTerminate = True? Потомок TThread возвращает ссылку, которую мне нужно проверить, но я не могу понять, как ждать завершения потока в тесте...

unit uThreadTests;

interface

uses
  Classes, TestFramework;

type

  TMyThread = class(TThread)
  strict private
    FId: Integer;
  protected
    procedure Execute; override;
  public
    constructor Create(AId: Integer);
    property Id: Integer read FId;
  end;

  TestTMyThread = class(TTestCase)
  strict private
    FMyId: Integer;
    procedure OnThreadTerminate(Sender: TObject);
  protected
    procedure SetUp; override;
    procedure TearDown; override;
  published
    procedure TestMyThread;
  end;

implementation

{ TMyThread }

constructor TMyThread.Create(AId: Integer);
begin
  FreeOnTerminate := True;
  FId := AId;

  inherited Create(False);
end;

procedure TMyThread.Execute;
begin
  inherited;

  FId := FId + 1;
end;

{ TestTMyThread }

procedure TestTMyThread.TestMyThread;
//var
//  LThread: TMyThread;
begin
//  LThread := TMyThread.Create(1);
//  LThread.OnTerminate := OnThreadTerminate;
//  LThread.WaitFor;
//  CheckEquals(2, FMyId);
//  LThread.Free;
///// The above commented out code is only useful of FreeOnTerminate = False;

  with TMyThread.Create(1) do
  begin
    OnTerminate := OnThreadTerminate;
    WaitFor; /// Not sure how else to wait for the thread to finish?
  end;

  CheckEquals(2, FMyId);
end;

procedure TestTMyThread.OnThreadTerminate(Sender: TObject);
begin
  FMyId := (Sender as TMyThread).Id;
end;  /// When FreeOnTerminate = True - THIS LINE CAUSES ERROR: Thread Error the handle is invalid

procedure TestTMyThread.SetUp;
begin
  inherited;

end;

procedure TestTMyThread.TearDown;
begin
  inherited;

end;

initialization
  RegisterTests([TestTMyThread.Suite]);


end.

Любые идеи будут приветствоваться.

Delphi 2010.

Ответ 1

Подклассифицируйте поток, чтобы сделать его более проверяемым. TThread и TObject обеспечивают достаточное количество крючков, которые вы можете добавить чувствительные переменные, чтобы заметить, что они достигают определенных точек с состояниями, которые вы хотите иметь.

Я вижу три аспекта для этого конкретного класса, которые вы, возможно, захотите проверить:

  • Он вычисляет значение для своего свойства Id на основе значения, отправленного конструктору.
  • Он вычисляет новое свойство Id в новом потоке, а не поток, вызывающий конструктор.
  • Он освобождается, когда он заканчивается.

Все эти вещи можно тестировать из подкласса, но трудно проверить иначе, не внося изменения в интерфейс потока. (Все остальные ответы до сих пор требуют изменения интерфейса потока, например, путем добавления дополнительных аргументов конструктора или изменения способа его запуска. Это может сделать поток более сложным или, по крайней мере, более громоздким, для использования в реальной программе.)

type
  PTestData = ^TTestData;
  TTestData = record
    Event: TEvent;
    OriginalId: Integer;
    FinalId: Integer;
  end;

  TTestableMyThread = class(TMyThread)
  private
    FData: PTestData;
  public
    constructor Create(AId: Integer; AData: PTestData);
    destructor Destroy; override;
    procedure AfterConstruction; override;
  end;

constructor TTestableMyThread.Create(AId: Integer; const AData: PTestData);
begin
  inherited Create(AId);
  FData := AData;
end;

destructor TestableMyThread.Destroy;
begin
  inherited;
  FData.FinalId := Id;
  // Tell the test that the thread has been freed
  FData.Event.SetEvent;
end;

procedure TTestableMyThread.AfterConstruction;
begin
  FData.OriginalId := Id;
  inherited; // Call this last because this is where the thread starts running
end;

Используя этот подкласс, можно написать тест, который проверяет три качества, идентифицированные ранее:

procedure TestTMyThread.TestMyThread;
var
  Data: TTestData;
  WaitResult: TWaitResult;
begin
  Data.OriginalId := -1;
  Data.FinalId := -1;
  Data.Event := TSimpleEvent.Create;
  try
    TTestableMyThread.Create(1, @Data);

    // We don't free the thread, and the event is only set in the destructor,
    // so if the event is signaled, it means the thread freed itself: That
    // aspect of the test implicitly passes. We don't want to wait forever,
    // though, so we fail the test if we have to wait too long. Either the
    // Execute method is taking too long to do its computations, or the thread
    // isn't freeing itself.
    // Adjust the timeout based on expected performance of Execute.
    WaitResult := Data.Event.WaitFor(5000);
    case WaitResult of
      wrSignaled: ; // This is the expected result
      wrTimeOut: Fail('Timed out waiting for thread');
      wrAbandoned: Fail('Event was abandoned');
      wrError: RaiseLastOSError(Data.Event.LastError);
      else Fail('Unanticipated error waiting for thread');
    end;

    CheckNotEquals(2, Data.OriginalId,
      'Didn''t wait till Execute to calculate Id');
    CheckEquals(2, Data.FinalId,
      'Calculated wrong Id value');
  finally
    Data.Event.Free;
  end;
end;

Ответ 2

Создайте поток в приостановленном состоянии, затем установите OnTerminate и, наконец, Resume поток.

В вашем тестовом классе определите личное логическое поле FThreadDone, которое инициализируется с помощью false и устанавливается true с помощью OnTerminate Eventhandler.

Кроме того, ваша логика конструктора немного грязна, так как вы не должны инициализировать поле до вызова унаследованного конструктора.

Итак:

constructor TMyThread.Create(AId: Integer);
begin
  inherited Create(true);
  FreeOnTerminate := True;
  FId := AId;
end;
...
procedure TestTMyThread.TestMyThread;
begin
  FThreadDone := False;
  with TMyThread.Create(1) do begin // Note: Thread is suspended...
    OnTerminate := OnThreadTerminate;
    // Resume;                         // ... and finally started here!
    Start;

  end;
  While not FThreadDone do Application.ProcessMessages;
  CheckEquals(2, FMyId);
end;

procedure TestTMyThread.OnThreadTerminate(Sender: TObject);
begin
  FMyId := (Sender as TMyThread).Id;
  FThreadDone := True;
end;

Это должно выполнить эту работу.

EDIT: исправлены глупые исправления, протестированы, работают.

Ответ 3

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

Решения, предлагаемые другими, а именно, просить поток сообщить вам, когда он заканчивается, хороши. Я лично, вероятно, решил бы сделать так. Если вы используете событие в качестве сигнала, вы можете ждать этого события.

Однако есть и другой способ сделать это.

  • Создать приостановленный поток.
  • Дублируйте дескриптор потока.
  • Запустите поток.
  • Дождитесь дублирования дескриптора.

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

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

{$APPTYPE CONSOLE}

uses
  SysUtils, Windows, Classes;

type
  TMyThread = class(TThread)
  protected
    procedure Execute; override;
  public
    destructor Destroy; override;
  end;

destructor TMyThread.Destroy;
begin
  Writeln('I''m dead!');
  inherited;
end;

procedure TMyThread.Execute;
begin
end;

var
  DuplicatedHandle: THandle;

begin
  with TMyThread.Create(True) do // must create suspended
  begin
    FreeOnTerminate := True;
    Win32Check(DuplicateHandle(
      GetCurrentProcess,
      Handle,
      GetCurrentProcess,
      @DuplicatedHandle,
      0,
      False,
      DUPLICATE_SAME_ACCESS
    ));
    Start;
  end;

  Sleep(500);
  Writeln('I''m waiting');
  if WaitForSingleObject(DuplicatedHandle, INFINITE)=WAIT_OBJECT_0 then
    Writeln('Wait succeeded');
  CloseHandle(DuplicatedHandle);
  Readln;
end.

Ответ 4

Вот пример использования анонимного потока.

  • Создано событие (TSimpleEvent)
  • Анонимный поток выполняет тестовый поток и
  • Ожидает событие, которое сигнализирует в обработчике OnTerminate тестового потока
  • Анонимный поток приостановлен до выполнения с помощью функции WaitFor
  • Результат был получен обработчиком OnTerminate

Важно то, что событие ждет в потоке. Отсутствие ситуации блокировки.


Uses
  SyncObjs;

type

  TMyThread = class(TThread)
  private
    FId : Integer;
  protected
    procedure Execute; override;
  public
    constructor Create( anInt : Integer);
    property Id : Integer read FId;
  end;

  TestTMyThread = class
  strict private
    FMyId: Integer;
    FMyEvent : TSimpleEvent;
    procedure OnThreadTerminate(Sender: TObject);
  protected
  public
    procedure TestMyThread;
  end;

{ TMyThread }

constructor TMyThread.Create(anInt : Integer);
begin
  inherited Create(True);
  FreeOnTerminate := True;
  FId := anInt;
end;

procedure TMyThread.Execute;
begin
  Inc(FId);
end;

procedure TestTMyThread.TestMyThread;
var
  AnonThread : TThread;
begin
  FMyEvent := TSimpleEvent.Create(nil,true,false,'');
  try
    AnonThread :=
      TThread.CreateAnonymousThread(
        procedure
        begin
          With TMyThread.Create(1) do
          begin
            OnTerminate := Self.OnThreadTerminate;
            Start;
          end;
          FMyEvent.WaitFor; // Wait until TMyThread is ready
        end
      );
    AnonThread.FreeOnTerminate := False;
    AnonThread.Start;

    AnonThread.WaitFor; // Wait here until test is ready
    AnonThread.Free;

    Assert(FMyId = 2); // Check result
  finally
    FMyEvent.Free;
  end;
end;

procedure TestTMyThread.OnThreadTerminate(Sender: TObject);
begin
  FMyId := (Sender as TMyThread).Id;
  FMyEvent.SetEvent; // Signal TMyThread ready
end;

Обновление, так как Delphi-2010 не имеет анонимного класса потоков, вот альтернатива, которую вы можете реализовать:

Type
  TMyAnonymousThread = class(TThread)
    private
      FProc : TProc;
    protected
      procedure Execute; override;
    public
      constructor Create(CreateSuspended,SelfFree: Boolean; const aProc: TProc);
  end;

constructor TMyAnonymousThread.Create(CreateSuspended,SelfFree: Boolean; 
  const aProc: TProc);
begin
  Inherited Create(CreateSuspended);
  FreeOnTerminate := SelfFree;
  FProc := aProc;
end;

procedure TMyAnonymousThread.Execute;
begin
  FProc();
end;