20

I'm trying to check the state of an object after running a test. This state is contained in a set. Is it possible to pass the expected state to the test case using DUnitX Attributes, so that I can use the same test for all different inputs?

I tried to pass the set as a constant or as a set, but in my Test routine it always arrives as an empty set.

  • Is this possible at all using attributes?
  • How would you test if sets are identical?

Example code:

type
  TResult = (resOK,resWarn,resError);
  TResultSet = set of TResult;

const
  cErrWarn : TResultSet = [resWarn];

type
  [TestFixture]
  TMyTest = class(TBaseTest)
    [Test]
    [TestCase('Demo1','InputA,[resWarn]')] // <-- tried as a set
    [TestCase('Demo2','InputB,cErrWarn')]  // <-- tried as a constant

    procedure Test(Input:string; ExpectedResult: TResultSet);
  end;

procedure TMyTest.Test(Input:string; ExpectedResult: TResultSet);
begin
  // ExpectedResult is always the empty set []
  RunTests(MyObject(Input));
  Assert.AreEqual(ExpectedResult, MyObject.ResultSet);
end;

I also tried to define the Expected result as array, but then DUnitX doesn't even call the test anymore. Probably that's just "too much"

    procedure Test(Input:string; ExpectedResult: array of TResult);

The best I could come up with so far was to use the following approach. Take a sample of up to three (insert your favourite integer here...) expected states and check for these separately. This is not really what I was hoping for, but it does the trick.

    procedure Test(Input:string; R1,R2,R3: TResult);

Help is greatly appreciated. :)

Lübbe Onken
  • 526
  • 2
  • 12
  • I would think you'd have to check using "IsTrue" and then make a comparison item by item. – Andrea Raimondi Jan 26 '16 at 09:11
  • that basically means that I have to pass in all items as separate parameters, which is what I wanted to avoid. – Lübbe Onken Jan 26 '16 at 09:20
  • I fundamentally disagree with that. The idea of unit testing is that you test each scenario separately. This means, you should have one test method per set value as appropriate: if more than one value is needed in a single test, then you should document the values you expect and then use those for your test. – Andrea Raimondi Jan 26 '16 at 10:45
  • 1
    I don't understand this comment @AndreaRaimondi. If I want to test the same object against five different data sets, I can either write five different tests with one data set each or I can write one test in DUnitX and user five different attribute sets to let DUnitX generate the five tests for me. That's just the thing I love about DUnitX – Lübbe Onken Jan 26 '16 at 11:20
  • I think you should write 5 different tests. There are several reasons for that, but the main one is that what works today in a certain way isn't guaranteed to be the same down the road. When (not if) a change happens, your test is going to fail. That's the whole point. If you let DUnitX generate tests for you, there may come a time where you need to separate them anyway. – Andrea Raimondi Jan 26 '16 at 16:28

2 Answers2

15

You are using TestCaseAttribute to specify the arguments to be passed to your test method. However, it simply does not offer any support for passing sets as arguments. You can see that this is so by looking at the constant Conversions declared in the DUnitX.Utils unit. It maps any conversion to a set to ConvFail.

So, if you want to specify this data using attributes you are going to need to extend the testing framework. You could derive your own descendent from CustomTestCaseSourceAttribute and override GetCaseInfoArray to decode your set arguments. As a crude example, you could use this:

type
  MyTestCaseAttribute = class(CustomTestCaseSourceAttribute)
  private
    FCaseInfo: TestCaseInfoArray;
  protected
    function GetCaseInfoArray: TestCaseInfoArray; override;
  public
    constructor Create(const ACaseName: string; const AInput: string; const AExpectedResult: TResultSet);
  end;

constructor MyTestCaseAttribute.Create(const ACaseName, AInput: string; const AExpectedResult: TResultSet);
begin
  inherited Create;
  SetLength(FCaseInfo, 1);
  FCaseInfo[0].Name := ACaseName;
  SetLength(FCaseInfo[0].Values, 2);
  FCaseInfo[0].Values[0] := TValue.From<string>(AInput);
  FCaseInfo[0].Values[1] := TValue.From<TResultSet>(AExpectedResult);
end;

function MyTestCaseAttribute.GetCaseInfoArray: TestCaseInfoArray;
begin
  Result := FCaseInfo;
end;

You can then add the following attribute to your test method:

[MyTestCase('Demo2', 'InputB', [resWarn])]
procedure Test(Input: string; ExpectedResult: TResultSet);

I've avoided using RTTI here for simplicity, but using RTTI would give you more flexibility. You'd pass the argument as a string, and decode it using RTTI, just as the code in DUnitX does. This means you don't need to write bespoke attributes every time you want to use a set argument.

Even better would be to implement this within DUnitX by extending the Conversions map to cover sets, and submit this as a patch. I'm sure others would find it useful.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Thanks for your reply David, I also started to look into the DUnitX sources, because I thought that it may be possible to add support for sets and arrays to it. Now at least I have a starting point. For the time being I'll use the R1,R2,R3 approach. That allows me to proceed with my project ... – Lübbe Onken Jan 26 '16 at 09:31
  • I wrote my first reply before your code example was visible here. Should press F5 more often :) Thanks a million! Now I'll check how far I'll come with your example. – Lübbe Onken Jan 26 '16 at 09:45
  • The code in the answer is a bit lame because it is specific to that one test method. Really you want to stand on top of RTTI. That is not too hard to do by providing `Conversions[tkUString, tkSet]`. – David Heffernan Jan 26 '16 at 09:54
15

Add this conversion function to DUnitX.Utils and put it into the Conversions matrix for tkUString to tkSet (due to limitations of StringToSet and TValue this only works for sets up to 32 elements, for larger sets of up to 256 elements there is more code required though):

function ConvStr2Set(const ASource: TValue; ATarget: PTypeInfo; out AResult: TValue): Boolean;
begin
  TValue.Make(StringToSet(ATarget, ASource.AsString), ATarget, AResult);
  Result := True;
end;

Also you need to use a different separator char for the parameters or it will split them wrong:

[TestCase('Demo1','InputA;[resWarn,resError]', ';')]
Stefan Glienke
  • 20,860
  • 2
  • 48
  • 102