delphi - test - ¿Puedo escribir pruebas ''parametrizadas'' en DUnit?
test en java (4)
¿Sería suficiente si DUnit permitiera escribir código como este, donde cada llamada de AddTestForDoStuff crearía un caso de prueba similar a los de su ejemplo?
Suite.AddTestForDoStuff.With(2).Expect(4);
Suite.AddTestForDoStuff.With(3).Expect(9);
Trataré de publicar un ejemplo de cómo se puede hacer más tarde hoy ...
Para .Net ya hay algo similar: Afirmaciones fluidas
http://www.codeproject.com/Articles/784791/Introduction-to-Unit-Testing-with-MS-tests-NUnit-a
Estoy usando DUnit para probar una biblioteca de Delphi. A veces me topo con casos en los que escribo varias pruebas muy similares para verificar entradas múltiples a una función.
¿Hay alguna manera de escribir (algo parecido) una prueba parametrizada en DUnit? Por ejemplo, especificar una entrada y un resultado esperado para un procedimiento de prueba adecuado, luego ejecutar el conjunto de pruebas y obtener retroalimentación sobre cuál de las múltiples ejecuciones de la prueba falló.
(Editar: un ejemplo)
Por ejemplo, supongamos que tengo dos pruebas como esta:
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;
Podría tener aún más de estas pruebas que hacen exactamente lo mismo pero con diferentes entradas y expectativas. No quiero fusionarlos en una sola prueba, porque me gustaría que pasen o fallen de forma independiente.
Aquí hay un ejemplo del uso de un método de prueba parametrizado general llamado desde los métodos de prueba reales (publicados) de los descendientes de 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í, hay algo de duplicación, pero la duplicación principal del código se tomó de estos métodos en los métodos SendAndReceive y CheckDestinationAgainstSource en una clase antepasado:
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;
El parámetro en CheckDestinationAgainstSource también permite este tipo de uso:
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;
Esta última prueba también podría codificarse usando la clase TRepeatedTest, pero creo que esa clase no es muy intuitiva de usar. El código anterior me da una mayor flexibilidad para codificar verificaciones y producir mensajes de falla inteligibles. Sin embargo, tiene el inconveniente de detener la prueba en la primera falla.
Creo que estás buscando algo como esto:
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.
que se ve así en el corredor de prueba GUI:
Estaría muy interesado en saber si he hecho esto de una manera subóptima. DUnit es tan increíblemente general y flexible que siempre que lo uso, siempre termino sintiendo que me he perdido una manera mejor y más simple de resolver el problema.
Puede usar DSharp para mejorar sus pruebas de DUnit. Especialmente la nueva unidad DSharp.Testing.DUnit.pas (en Delphi 2010 y superior).
Solo agréguela a sus usos después de TestFramework y puede agregar atributos a su caso de prueba. Entonces podría verse así:
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.
Cuando lo ejecutas, tu prueba se ve así:
Como los atributos en Delphi solo aceptan constantes, los atributos simplemente toman los argumentos como una cadena donde los valores están separados por un punto y coma. Pero nada le impide crear sus propias clases de atributos que toman múltiples argumentos del tipo correcto para evitar cadenas "mágicas". De todos modos, estás limitado a tipos que pueden ser const.
También puede especificar el atributo de Valores en cada argumento del método y se llama con cualquier combinación posible (como en NUnit ).
Al referirme a las otras respuestas personalmente, quiero escribir la menor cantidad de código posible al escribir pruebas unitarias. También quiero ver lo que hacen las pruebas cuando miro la parte de la interfaz sin profundizar en la parte de implementación (no voy a decir: "hagamos BDD "). Es por eso que prefiero la forma declarativa.