Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Delphi unit test for a TThread with FreeOnTerminate = True

What is the best way to write a Delphi DUnit test for a TThread descendant when FreeOnTerminate = True? The TThread descendant returns a reference which I need to test for, but I can't figure out how to wait for the thread to finish in the test...

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.

Any ideas would be welcomed.

Delphi 2010.

like image 921
Rick Wheeler Avatar asked Mar 20 '13 02:03

Rick Wheeler


1 Answers

Subclass the thread to make it more testable. TThread and TObject provide enough hooks that you can add sensing variables to observe that it reaches certain points with the states you want it to have.

I see three aspects to this particular class that you might wish to test:

  1. It computes a value for its Id property based on the value sent to the constructor.
  2. It computes the new Id property in the new thread, not the thread that calls the constructor.
  3. It frees itself when it's finished.

All those things are testable from a subclass, but hard to test otherwise without making changes to the thread's interface. (All the other answers so far require changing the thread's interface, such as by adding more constructor arguments or by changing the way it starts itself. That can make the thread harder, or at least more cumbersome, to use in the real program.)

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;

Using that subclass, it's possible to write a test that checks the three qualities identified earlier:

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;
like image 133
Rob Kennedy Avatar answered Oct 14 '22 02:10

Rob Kennedy