2012-01-25 16 views
20

Sto usando DUnit per testare una libreria Delphi. A volte mi imbatto in casi, in cui scrivo diversi test molto simili per verificare più ingressi in una funzione.Posso scrivere test "parametrizzati" in DUnit

C'è un modo per scrivere (qualcosa di simile a un test parametrizzato in DUnit? Ad esempio, specificare un input e un output atteso in una procedura di test adatta, quindi eseguire la suite di test e ottenere un feedback su quale delle più analisi del test non è riuscita?

(Edit: un esempio)

Per esempio, supponiamo che ho avuto due test come questo:

procedure TestMyCode_WithInput2_Returns4(); 
var 
    Sut: TMyClass; 
    Result: Integer; 
begin 
    // Arrange: 
    Sut := TMyClass.Create; 

    // Act: 
    Result := sut.DoStuff(2); 

    // Assert 
    CheckEquals(4, Result); 
end; 

procedure TestMyCode_WithInput3_Returns9(); 
var 
    Sut: TMyClass; 
    Result: Integer; 
begin 
    // Arrange: 
    Sut := TMyClass.Create; 

    // Act: 
    Result := sut.DoStuff(3); 

    // Assert 
    CheckEquals(9, Result); 
end; 

potrei avere ancora di più di questi test che fanno esattamente la stessa cosa, ma con diversi input e aspettative. Non voglio unirli in un test, perché vorrei che fossero in grado di passare o fallire indipendentemente.

+1

Intendi la creazione dinamica di casi di test per tutti i valori di input in un elenco? Il mio (piccolo) [OpenCTF] (http://sourceforge.net/projects/openctf/) framework di test contiene il codice per la creazione dinamica di casi di test. È basato su DUnit. – mjn

+0

È sempre possibile scrivere un metodo parametrico generale nella classe di test e chiamarlo da uno o più metodi di test specifici (pubblicati). Il metodo Check (Not) Equals di un TestCase può aiutare anche qui a mantenere il codice conciso e fornire comunque un messaggio di errore specifico per ogni test. –

+0

@Marjan il metodo di test interrompe l'esecuzione non appena il primo Check (Not) Equals non riesce - la creazione dinamica di test case risolve questo problema, tutti gli altri valori saranno comunque testati – mjn

risposta

3

Sarebbe sufficiente se DUnit consentisse di scrivere codice come questo, dove ogni chiamata di AddTestForDoStuff creerebbe un caso di test simile a quelli nell'esempio?

Suite.AddTestForDoStuff.With(2).Expect(4); 
Suite.AddTestForDoStuff.With(3).Expect(9); 

Cercherò di postare un esempio di come questo può essere fatto entro oggi ...


per NET c'è già qualcosa di simile: asserzioni Fluent

http://www.codeproject.com/Articles/784791/Introduction-to-Unit-Testing-with-MS-tests-NUnit-a

+0

Qualcosa di simile sarebbe bello. Ma probabilmente dovrebbe anche prendere il test specifico come argomento, giusto? Ad esempio 'Suite.AddTest ('DoStuff'). WithArgument (2) .Expects (4)' –

+0

@MathiasFalkenberg: Questa o almeno la possibilità di aggiungere un messaggio. –

+0

Questa risposta è abbastanza interessante. E 'la prima volta che vedo un voto alzato per un pio desiderio. Sì, non sarebbe fantastico se tu potessi fare questo !! In ogni caso, il pagamento dell'anticipo +1 sarà guadagnato se è possibile produrre il codice che effettivamente segue. –

0

Ecco un esempio di utilizzo di un metodo di prova parametrico generale chiamato dai metodi di test effettivi (pubblicati) dei discendenti di TTestCase (:

procedure TTester.CreatedWithoutDisplayFactorAndDisplayString; 
begin 
    MySource := TMyClass.Create(cfSum); 

    SendAndReceive; 
    CheckDestinationAgainstSource; 
end; 

procedure TTester.CreatedWithDisplayFactorWithoutDisplayString; 
begin 
    MySource := TMyClass.Create(cfSubtract, 10); 

    SendAndReceive; 
    CheckDestinationAgainstSource; 
end; 

Sì, c'è un po 'di duplicazione, ma la duplicazione principale del codice è stato preso fuori di questi metodi in metodi SendAndReceive e CheckDestinationAgainstSource in una classe antenata:

procedure TCustomTester.SendAndReceive; 
begin 
    MySourceBroker.CalculationObject := MySource; 
    MySourceBroker.SendToProtocol(MyProtocol); 
    Check(MyStream.Size > 0, 'Stream does not contain xml data'); 
    MyStream.Position := 0; 
    MyDestinationBroker.CalculationObject := MyDestination; 
    MyDestinationBroker.ReceiveFromProtocol(MyProtocol); 
end; 

procedure TCustomTester.CheckDestinationAgainstSource(const aCodedFunction: string = ''); 
var 
    ok: Boolean; 
    msg: string; 
begin 
    if aCodedFunction = '' then 
    msg := 'Calculation does not match: ' 
    else 
    msg := 'Calculation does not match. Testing CodedFunction ' + aCodedFunction + ': '; 

    ok := MyDestination.IsEqual(MySource, MyErrors); 
    Check(Ok, msg + MyErrors.Text); 
end; 

Il parametro nel CheckDestinationAgainstSource permette anche per questo tipo di utilizzo:

procedure TAllTester.AllFunctions; 
var 
    CF: TCodedFunction; 
begin 
    for CF := Low(TCodedFunction) to High(TCodedFunction) do 
    begin 
    TearDown; 
    SetUp; 
    MySource := TMyClass.Create(CF); 
    SendAndReceive; 
    CheckDestinationAgainstSource(ConfiguredFunctionToString(CF)); 
    end; 
end; 

Quest'ultimo test potrebbe anche essere codificato utilizzando la classe TRepeatedTest, ma trovo che classe piuttosto intuitivo da usare. Il codice sopra riportato mi offre una maggiore flessibilità nella codifica dei controlli e nella produzione di messaggi di errore comprensibili. Ha tuttavia lo svantaggio di fermare il test al primo errore.

11

penso che siete alla ricerca di qualcosa di simile:

unit TestCases; 

interface 

uses 
    SysUtils, TestFramework, TestExtensions; 

implementation 

type 
    TArithmeticTest = class(TTestCase) 
    private 
    FOp1, FOp2, FSum: Integer; 
    constructor Create(const MethodName: string; Op1, Op2, Sum: Integer); 
    public 
    class function CreateTest(Op1, Op2, Sum: Integer): ITestSuite; 
    published 
    procedure TestAddition; 
    procedure TestSubtraction; 
    end; 

{ TArithmeticTest } 

class function TArithmeticTest.CreateTest(Op1, Op2, Sum: Integer): ITestSuite; 
var 
    i: Integer; 
    Test: TArithmeticTest; 
    MethodEnumerator: TMethodEnumerator; 
    MethodName: string; 
begin 
    Result := TTestSuite.Create(Format('%d + %d = %d', [Op1, Op2, Sum])); 
    MethodEnumerator := TMethodEnumerator.Create(Self); 
    Try 
    for i := 0 to MethodEnumerator.MethodCount-1 do begin 
     MethodName := MethodEnumerator.NameOfMethod[i]; 
     Test := TArithmeticTest.Create(MethodName, Op1, Op2, Sum); 
     Result.addTest(Test as ITest); 
    end; 
    Finally 
    MethodEnumerator.Free; 
    End; 
end; 

constructor TArithmeticTest.Create(const MethodName: string; Op1, Op2, Sum: Integer); 
begin 
    inherited Create(MethodName); 
    FOp1 := Op1; 
    FOp2 := Op2; 
    FSum := Sum; 
end; 

procedure TArithmeticTest.TestAddition; 
begin 
    CheckEquals(FOp1+FOp2, FSum); 
    CheckEquals(FOp2+FOp1, FSum); 
end; 

procedure TArithmeticTest.TestSubtraction; 
begin 
    CheckEquals(FSum-FOp1, FOp2); 
    CheckEquals(FSum-FOp2, FOp1); 
end; 

function UnitTests: ITestSuite; 
begin 
    Result := TTestSuite.Create('Addition/subtraction tests'); 
    Result.AddTest(TArithmeticTest.CreateTest(1, 2, 3)); 
    Result.AddTest(TArithmeticTest.CreateTest(6, 9, 15)); 
    Result.AddTest(TArithmeticTest.CreateTest(-3, 12, 9)); 
    Result.AddTest(TArithmeticTest.CreateTest(4, -9, -5)); 
end; 

initialization 
    RegisterTest('My Test cases', UnitTests); 

end. 

che assomiglia a questo nel test GUI corridore:

enter image description here

Sarei molto interessato a sapere se ho sono andati su questo in modo sub-ottimale. DUnit è così incredibilmente generale e flessibile che ogni volta che lo uso mi sento sempre come se avessi perso un modo migliore e più semplice per risolvere il problema.

+2

Mi sento allo stesso modo ... Ecco perché ho postato questa domanda. Mentre il tuo codice produce sicuramente l'output desiderato, vorrei che i miei test fossero più leggibili. Il metodo "CreateTest" introduce uno strato di complessità nel codice di test che preferirei davvero evitare ... –

19

È possibile utilizzare DSharp per migliorare i test DUnit. Soprattutto la nuova unità DSharp.Testing.DUnit.pas (in Delphi 2010 e versioni successive).

Basta aggiungerlo ai propri usi dopo TestFramework ed è possibile aggiungere attributi al caso di test. Poi si potrebbe assomigliare a questo:

unit MyClassTests; 

interface 

uses 
    MyClass, 
    TestFramework, 
    DSharp.Testing.DUnit; 

type 
    TMyClassTest = class(TTestCase) 
    private 
    FSut: TMyClass; 
    protected 
    procedure SetUp; override; 
    procedure TearDown; override; 
    published 
    [TestCase('2;4')] 
    [TestCase('3;9')] 
    procedure TestDoStuff(Input, Output: Integer); 
    end; 

implementation 

procedure TMyClassTest.SetUp; 
begin 
    inherited; 
    FSut := TMyClass.Create; 
end; 

procedure TMyClassTest.TearDown; 
begin 
    inherited; 
    FSut.Free; 
end; 

procedure TMyClassTest.TestDoStuff(Input, Output: Integer); 
begin 
    CheckEquals(Output, FSut.DoStuff(Input)); 
end; 

initialization 
    RegisterTest(TMyClassTest.Suite); 

end. 

Quando si esegue il test si presenta così:

enter image description here

Dal momento che gli attributi in Delphi solo accettare costanti gli attributi basta prendere gli argomenti come una stringa in cui i valori sono separati da un punto e virgola. Ma nulla ti impedisce di creare le tue classi di attributi che accettano più argomenti del tipo corretto per evitare stringhe "magiche". Ad ogni modo sei limitato a tipi che possono essere costanti.

È inoltre possibile specificare l'attributo Valori su ciascun argomento del metodo e viene chiamato con qualsiasi combinazione possibile (come in NUnit).

In riferimento alle altre risposte personalmente, desidero scrivere il minor numero possibile di codice durante la scrittura dei test di unità. Voglio anche vedere che cosa fanno i test quando guardo la parte dell'interfaccia senza scavare attraverso la parte di implementazione (non dirò: "facciamolo BDD"). Questo è il motivo per cui preferisco il modo dichiarativo.

+0

+1 Sembra davvero molto utile e interessante.Grazie per averlo portato alla nostra attenzione. –

+0

+1 Infatti! Sarò sicuro di esaminarlo. Penso che questo sia l'approccio più semplice suggerito finora! –