2013-03-20 5 views
12

Qual è il modo migliore per scrivere un test DUit di Delphi per un discendente TThread quando FreeOnTerminate = True? Il discendente TThread restituisce un riferimento che ho bisogno di testare per, ma non riesco a capire come attendere per il thread per finire nel test ...Test unità Delphi per TThread con FreeOnTerminate = True

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. 

Tutte le idee sarebbe il benvenuto.

Delphi 2010.

risposta

3

Sottoclasse la filettatura per renderla più verificabile. TThread e TObject forniscono abbastanza hook che è possibile aggiungere variabili di rilevamento per osservare che raggiunge determinati punti con gli stati che si desidera che abbiano.

Vedo tre aspetti di questa particolare classe che si potrebbe desiderare di prova:

  1. Si calcola un valore per la relativa proprietà Id in base al valore inviato al costruttore.
  2. Calcola la nuova proprietà Id nel nuovo thread, non il thread che chiama il costruttore.
  3. Si libera da solo quando è finito.

Tutte queste cose sono testabili da una sottoclasse, ma difficili da testare altrimenti senza apportare modifiche all'interfaccia del thread. (Tutte le altre risposte richiedono finora la modifica dell'interfaccia del thread, ad esempio aggiungendo ulteriori argomenti del costruttore o modificando il modo in cui viene avviata. Ciò può rendere il thread più difficile, o almeno più ingombrante, da utilizzare nel programma reale.)

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; 

utilizzando tale sottoclasse, è possibile scrivere un test che verifica le tre qualità individuati in precedenza:

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; 
+0

Grazie per la risposta Rob. Non ho avuto il tempo di testare la soluzione, ma ci proverò domani. –

+0

Ciao Rob, grazie la tua risposta sembra funzionare alla grande. Ho accettato questa come risposta. Inoltre, c'era una risposta interessante pubblicata qui sotto usando thread anonimi. Sono ancora su Delphi 2010, quindi non ho accesso ad esso, ma sembra una soluzione elegante. Verificherò anche questo una volta che avrò aggiornato. Saluti. –

2

Creare il filo in uno stato sospeso, quindi impostare il OnTerminate ed infine Resume il filo.

Nella tua classe di test, definire un campo booleano privato FThreadDone che viene inizializzato con false e impostato true dal OnTerminate EventHandler.

Inoltre, la logica del costruttore è un po 'sporca, poiché non è necessario inizializzare il campo prima di chiamare il costruttore ereditato.

Quindi:

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; 

Questo dovrebbe fare il lavoro.

MODIFICA: correzioni stupide corrette, testate, funzionanti.

+0

L'unico posto che sono l'accesso alla gestione thread è nel gestore 'OnTerminate' che è chiamato * prima * il thread è stato liberato. Si noti che creo il thread in uno stato sospeso e lo riprendo manualmente. – alzaimar

+0

@DavidHeffernan: Nessun problema. – alzaimar

+2

Il resume è deprecato nelle nuove versioni di Delphi. In quelli che dovresti usare Start dopo aver creato un thread in sospeso. –

2

Poiché hai reso il thread libero al momento della chiusura, hai chiesto di distruggere tutte le tracce di se stesso non appena fatto. Dal momento che non è possibile esercitare alcuna influenza quando finisce, è sbagliato fare riferimento a qualsiasi cosa all'interno del thread dopo averlo avviato.

Le soluzioni proposte da altri, ovvero chiedere al thread di segnalare quando termina, sono buone. Io personalmente probabilmente sceglierei di farlo in quel modo. Se usi un evento come segnale, puoi aspettare quell'evento.

Tuttavia, c'è un altro modo per farlo.

  1. Creare il thread sospeso.
  2. Duplica la maniglia del filo.
  3. Avvia la discussione.
  4. Attendere l'handle duplicato.

Poiché si possiede l'handle duplicato, anziché il thread, si è sicuri di attendere. Sembra un po 'più complicato, ma suppongo che eviti di creare un oggetto di sincronizzazione aggiuntivo in cui uno non è necessario. Si noti che non sto sostenendo questo approccio rispetto all'approccio all'uso di un evento per segnalare il completamento.

In ogni caso, ecco una semplice dimostrazione dell'idea.

{$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. 
+0

Grazie David, non ho testato la tua soluzione principalmente a causa della mancanza di tempo al momento. Sono andato con la seconda risposta di Rob per ora, ma quando aggiornerò da Delphi 2010 proverò anche la risposta qui sotto usando thread anonimi che sembrano davvero interessanti. Grazie ancora. –

1

Ecco un esempio utilizzando un filo anonimo. Viene creato

  • Un evento (TSimpleEvent)
  • Un filo anonima esegue il filo di prova e
  • attende l'evento, che segnala nel gestore OnTerminate del filo di prova
  • Il filo anonimo è il tenere premuto fino eseguito con un WaitFor
  • il risultato è stato raccolto dal gestore OnTerminate

la cosa importante qui è che l'evento è atteso in una discussione. Nessuna situazione di dead-lock.


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; 

aggiornamento, dal momento che Delphi-2010 non ha una classe filo anonima, ecco un'alternativa che è possibile implementare:

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; 
+0

Mi piacerebbe provare questo, ma sembra che i thread anonimi siano entrati in Delphi XE, ma sono ancora su Delphi 2010. Un altro motivo per aggiornare ... Grazie per la tua risposta, quando eseguo l'aggiornamento, rivisiterò e vedrò se funziona. –