In this article I am playing with
Unit tests and
TCustomAttributes as I am working on different ideas to build up a lightweight testing framework. Basically I liked the idea of
TestCase attribute from
NUnit and I wanted to do something similar using TCustomAttributes and accessing those attributes using the
Delphi RTTI library. To understand better my purposes have a look at the following example extracted from
NUnit webpage:
[TestCase(12,3,4)]
[TestCase(12,2,6)]
[TestCase(12,4,3)]
public void DivideTest(int n, int d, int q)
{
Assert.AreEqual( q, n / d );
}
This simple example will execute three times the method or test using the parameters defined on the test case attributes. What if we could perform something similar? Would not be cool?
Here is what I have done so far:
Creation of the Custom Attributes:
type
TUserPasswordAttribute = class(TCustomAttribute)
private
FPassword: string;
FUserName: string;
Fresponse: Boolean;
procedure SetPassword(const Value: string);
procedure SetUserName(const Value: string);
procedure Setresponse(const Value: Boolean);
public
constructor Create(aUserName: string; aPassword: string; aResponse : Boolean);
property UserName: string read FUserName write SetUserName;
property Password: string read FPassword write SetPassword;
property response : Boolean read Fresponse write Setresponse;
end;
TUserAgeAttribute = class(TCustomAttribute)
private
FAge: integer;
FUserName: String;
Fresponse: Boolean;
procedure SetAge(const Value: integer);
procedure SetUserName(const Value: String);
procedure Setresponse(const Value: Boolean);
public
property UserName : String read FUserName write SetUserName;
property Age : integer read FAge write SetAge;
property response : Boolean read Fresponse write Setresponse;
constructor Create(aUserName : string; aAge : Integer; aResponse : Boolean);
end;
{ TUserPasswordAttribute }
constructor TUserPasswordAttribute.Create(aUserName, aPassword: string; aResponse : Boolean);
begin
SetUserName(aUserName);
SetPassword(aPassword);
Setresponse(aResponse);
end;
procedure TUserPasswordAttribute.SetPassword(const Value: string);
begin
FPassword := Value;
end;
procedure TUserPasswordAttribute.Setresponse(const Value: Boolean);
begin
Fresponse := Value;
end;
procedure TUserPasswordAttribute.SetUserName(const Value: string);
begin
FUserName := Value;
end;
{ TUserAgeAttribute }
constructor TUserAgeAttribute.Create(aUserName: string; aAge: Integer; aResponse : Boolean);
begin
SetUserName(aUserName);
SetAge(aAge);
Setresponse(aResponse);
end;
procedure TUserAgeAttribute.SetAge(const Value: integer);
begin
FAge := Value;
end;
procedure TUserAgeAttribute.Setresponse(const Value: Boolean);
begin
Fresponse := Value;
end;
procedure TUserAgeAttribute.SetUserName(const Value: String);
begin
FUserName := Value;
end;
Those two custom attributes will serve as an example for what I intend to do. I need to test a login and some data from a current user and those bespoke attributes would be used by the test case.
The Framework:
type
TAttributeProc = reference to procedure(CustomAttr: TCustomAttribute);
TFrameworkTestCase = class(TTestCase)
public
procedure TestAttributesMethod(CustomProc: TAttributeProc);
end;
implementation
{ TFrameworkTestCase }
procedure TFrameworkTestCase.TestAttributesMethod(CustomProc: TAttributeProc);
var
ContextRtti: TRttiContext;
RttiType: TRttiType;
RttiMethod: TRttiMethod;
CustomAttr: TCustomAttribute;
begin
ContextRtti := TRttiContext.Create;
try
RttiType := ContextRtti.GetType(Self.ClassType);
for RttiMethod in RttiType.GetMethods do
for CustomAttr in RttiMethod.GetAttributes do
CustomProc(CustomAttr);
finally
ContextRtti.Free;
end;
end;
As you can see TFrameworkTestCase inherits from TTestCase and it adds the magic of reading the custom attributes and invoking the delegate as many times needed.
Using the small framework:
unit TestUnit1;
{
Delphi DUnit Test Case
----------------------
This unit contains a skeleton test case class generated by the Test Case Wizard.
Modify the generated code to correctly setup and call the methods from the unit
being tested.
}
interface
uses
TestFramework, Windows, Forms, Dialogs, Controls, Classes, RTTI, SysUtils, Variants,
Graphics, Messages, Unit1, StdCtrls;
type
TestTLoginCase = class(TFrameworkTestCase)
strict private
FLogin: TLogin;
public
procedure SetUp; override;
procedure TearDown; override;
published
[TUserPasswordAttribute('User1', 'Password1', True)]
[TUserPasswordAttribute('User2', 'Password2', True)]
[TUserPasswordAttribute('User3', 'Password3', True)]
[TUserPasswordAttribute('User3', '', False)]
procedure TestUserLogin;
[TUserAgeAttribute('User1', 26, True)]
[TUserAgeAttribute('User2', 27, True)]
[TUserAgeAttribute('User3', 28, False)]
procedure TestUserAge;
end;
implementation
procedure TestTLoginCase.SetUp;
begin
FLogin := TLogin.Create;
end;
procedure TestTLoginCase.TearDown;
begin
FLogin.Free;
FLogin := nil;
end;
procedure TestTLoginCase.TestUserAge;
var
aAge: integer;
aUserName: string;
aResponse : boolean;
begin
TestAttributesMethod(procedure (CustomAttr : TCustomAttribute)
begin
if CustomAttr is TUserAgeAttribute then
begin
aUserName := TUserAgeAttribute(CustomAttr).UserName;
aAge := TUserAgeAttribute(CustomAttr).Age;
aResponse := TUserAgeAttribute(CustomAttr).Response;
Assert(aResponse=(FLogin.fetchDatauser(aUserName)=aAge), 'Incorrect value ' + aUserName);
end;
end);
end;
procedure TestTLoginCase.TestUserLogin;
var
aPassword: string;
aUserName: string;
aResponse : boolean;
begin
TestAttributesMethod(procedure (CustomAttr : TCustomAttribute)
begin
if CustomAttr is TUserPasswordAttribute then
begin
aUserName := TUserPasswordAttribute(CustomAttr).UserName;
aPassword := TUserPasswordAttribute(CustomAttr).Password;
aResponse := TUserPasswordAttribute(CustomAttr).Response;
Assert(FLogin.UserLogin(aUserName, aPassword)=aResponse, 'Incorrect user ' + aUserName);
end;
end);
end;
initialization
RegisterTest(TestTLoginCase.Suite);
end.
Notice that every test case contains a set of Custom attributes and they will be executed by using a delegate. I have included the Result parameter in the attribute so the test can know the result straight away and inform about it.
Related links: