Search code examples
delphiunit-testingdelphi-2010dunittthread

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.


Solution

  • 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;