Могу ли я писать параметризованные тесты в DUnit

Я использую DUnit для тестирования библиотеки Delphi. Я иногда сталкиваюсь с случаями, когда я пишу несколько очень похожих тестов для проверки нескольких входов в функцию.

Есть ли способ написать (что-то похожее) параметризованный тест в DUnit? Например, указание ввода и ожидаемого вывода для подходящей процедуры тестирования, затем запуск набора тестов и получение обратной связи о том, какой из нескольких запусков теста завершился неудачно?

(Изменить: пример)

Например, предположим, что у меня было два таких теста:

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;

У меня может быть еще больше тестов, которые делают одно и то же, но с разными исходными данными и ожиданиями. Я не хочу объединять их в один тест, потому что я бы хотел, чтобы они могли сдать или не сдать экзамен независимо.


person Mathias Falkenberg    schedule 25.01.2012    source источник
comment
Вы имеете в виду динамическое создание тестовых примеров для всех входных значений в списке? Моя (небольшая) структура тестирования OpenCTF содержит код для динамического создания тестовых примеров. Он основан на DUnit.   -  person mjn    schedule 25.01.2012
comment
Вы всегда можете написать общий параметризованный метод в тестовом классе и вызвать его из одного или нескольких конкретных (опубликованных) методов тестирования. Здесь также могут помочь метод (ы) Check (Not) Equals TestCase, чтобы помочь сохранить краткость кода и при этом предоставить конкретное сообщение об ошибке для каждого теста.   -  person Marjan Venema    schedule 25.01.2012
comment
@Marjan метод тестирования прекратит выполнение, как только первая проверка (Not) Equals завершится неудачно - динамическое создание тестовых примеров решает эту проблему, все остальные значения все равно будут проверяться   -  person mjn    schedule 25.01.2012
comment
@mjn: OpenCTF, похоже, предназначен для тестирования компонентов и форм в стиле черного ящика ... Это здесь не применимо ...   -  person Mathias Falkenberg    schedule 25.01.2012
comment
@MarjanVenema: Думаю, это неплохой способ сделать это. Я попробую это ...   -  person Mathias Falkenberg    schedule 25.01.2012
comment
@MathiasFalkenberg: не могли бы вы подробнее рассказать, какие тесты вы пытаетесь написать? Теперь я не уверен, хотите ли вы использовать одну и ту же функцию с несколькими входами или хотите ли вы проверять входы в функции? Другими словами: пожалуйста, будьте более конкретны в отношении тестовых сценариев, которые вы хотите запустить, чтобы мы могли быть более точными с возможными вариантами. (@mjn и я, по крайней мере, интерпретируем это по-разному).   -  person Marjan Venema    schedule 25.01.2012
comment
@MarjanVenema: Я пробовал добавить пример. Что я хотел бы сделать, так это реализовать те же функции с разными входными данными, а затем проверить результат.   -  person Mathias Falkenberg    schedule 25.01.2012


Ответы (4)


Вы можете использовать DSharp для улучшения ваших тестов DUnit. Особенно новый модуль DSharp.Testing .DUnit.pas (в Delphi 2010 и выше).

Просто добавьте его к своему использованию после TestFramework, и вы сможете добавлять атрибуты в свой тестовый пример. Тогда это могло бы выглядеть так:

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.

Когда вы запустите его, ваш тест будет выглядеть так:

введите описание изображения здесь

Поскольку атрибуты в Delphi просто принимают константы, атрибуты просто принимают аргументы в виде строки, в которой значения разделяются точкой с запятой. Но ничто не мешает вам создавать свои собственные классы атрибутов, которые принимают несколько аргументов правильного типа, чтобы предотвратить «волшебные» строки. В любом случае вы ограничены типами, которые могут быть константными.

Вы также можете указать атрибут Values ​​для каждого аргумента метода, и он будет вызываться с любой возможной комбинацией (например, NUnit).

Что касается других ответов, я хочу писать как можно меньше кода при написании модульных тестов. Также я хочу увидеть, что делают тесты, когда я смотрю на интерфейсную часть, не копаясь в части реализации (я не собираюсь говорить: «давайте сделаем BDD"). Поэтому я предпочитаю декларативный способ.

person Stefan Glienke    schedule 25.01.2012
comment
+1 Выглядит действительно очень полезно и интересно. Спасибо, что обратили на это наше внимание. - person David Heffernan; 25.01.2012
comment
+1 Действительно! Я обязательно займусь этим. Я думаю, что это самый простой из предложенных на данный момент подходов! - person Mathias Falkenberg; 26.01.2012

Думаю, вы ищете что-то вроде этого:

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.

что выглядит так в средстве выполнения тестов с графическим интерфейсом пользователя:

введите описание изображения здесь

Мне было бы очень интересно узнать, сделал ли я это неоптимальным образом. DUnit настолько универсален и гибок, что всякий раз, когда я использую его, я всегда чувствую, что упустил лучший и более простой способ решить проблему.

person David Heffernan    schedule 25.01.2012
comment
Я чувствую то же самое ... Вот почему я разместил этот вопрос. Хотя ваш код, безусловно, дает желаемый результат, я хотел бы, чтобы мои тесты были более читабельными. Метод CreateTest вводит уровень сложности в тестовый код, которого я бы предпочел избежать ... - person Mathias Falkenberg; 26.01.2012

Было бы достаточно, если бы DUnit позволял писать такой код, где каждый вызов AddTestForDoStuff создавал бы тестовый пример, аналогичный тем, что в вашем примере?

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

Я постараюсь опубликовать пример, как это можно сделать, позже сегодня ...


Для .Net уже есть нечто подобное: Fluent Assertions

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

person mjn    schedule 25.01.2012
comment
Что-нибудь в этом роде было бы неплохо. Но, вероятно, нужно будет использовать и конкретный тест в качестве аргумента, не так ли? Такие как Suite.AddTest('DoStuff').WithArgument(2).Expects(4) - person Mathias Falkenberg; 25.01.2012
comment
@MathiasFalkenberg: Это или, по крайней мере, возможность добавить сообщение. - person Marjan Venema; 25.01.2012
comment
Это довольно крутой ответ. Я впервые вижу, как за принятие желаемого за действительное голосование награждается. Да, было бы здорово, если бы ты мог это сделать !! В любом случае, авансовый платеж +1 будет заработан, если вы сможете создать код, который действительно выполняет. - person David Heffernan; 25.01.2012
comment
Подсказка: AddTestForDoStuff создает экземпляр TTestDoStuff, With и Expect работают как установщики свойств. Это шаблон построителя, примененный к DUnit. - person mjn; 25.01.2012

Вот пример использования общего параметризованного метода тестирования, вызываемого из фактических (опубликованных) методов тестирования ваших потомков TTestCase (:

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

  SendAndReceive;
  CheckDestinationAgainstSource;
end;

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

  SendAndReceive;
  CheckDestinationAgainstSource;
end;

Да, есть некоторое дублирование, но основное дублирование кода было перенесено из этих методов в методы SendAndReceive и CheckDestinationAgainstSource в классе-предке:

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;

Параметр в CheckDestinationAgainstSource также позволяет использовать этот тип:

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;

Этот последний тест можно также закодировать с использованием класса TRepeatedTest, но я считаю, что этот класс довольно неинтуитивно использовать. Приведенный выше код дает мне большую гибкость в написании проверок и создании понятных сообщений об ошибках. Однако у него есть недостаток, заключающийся в остановке теста при первом сбое.

person Marjan Venema    schedule 25.01.2012