Delphi Mocks – Is it possible to use ‘VAR’ or ‘OUT’ arrangements in a function that is been mocked with a ‘WillReturn’?

StackOverflow https://stackoverflow.com/questions/18272646

Question

I have just started to use Delphi-Mocks with my dunit tests but it has little or no documentation.

The Problem is:

I am trying to write a test ‘Test_LogonUser_CheckPwd_GOOD_PASSWORD’

But I am not sure how to mock out the function Fusers.CheckPwd(TEST_USERID,TEST_PASSWORD, ERROR_CODE);

Normally I would use: Fusers.Setup.WillReturn(True).When.CheckPwd(TEST_USERID,TEST_PASSWORD, ERROR_CODE);

But ‘ERROR_CODE’ is an OUT value and gives a compile error

QUESTIONS:

  1. Is there a way to make ‘WillReturn’ work with OUT or VAR parameters ?
  2. Is there a different way to mock out ‘CheckPwd’ so that it looks at OUT or VAR parameters ?

Here is my code:

/////  TDlUsers
interface
type
{$M+}
  IDlUsers = Interface(IInterface)
 ['{3611B437-888C-4919-B304-238A80DAD476}']
    function   VerifyPassword(UserId: integer; Pwd: string): Boolean;
    function   CheckPwd(UserId: integer; Pwd: string; out ErrorCode: Integer) : Boolean;
  end;
  function  Dl_Users : IDlUsers;
{$M-}
implementation
type
  TDlUsers = class(TDbIControl,IDlUsers)
  public
    function   VerifyPassword(UserId: integer; Pwd: string): Boolean;
    function   CheckPwd(UserId: integer; Pwd: string; out ErrorCode: Integer; out ErrorMsg:String) : Boolean;
  end;

  function  Dl_Users : IDlUsers;
  begin
    result :=  TDlUsers.create;
  end;

////  TUserCtrl

interface
uses DlUsers;
type
  TUserCtrl = class
  private
    FUsers      : IDlUsers;
  public
    function    LogonUser_CheckPwd(UserID: Integer; Pwd: String): Boolean;
    function    LogonUser_VerifyPassword(UserID: Integer; Pwd: String): Boolean;
    constructor Create;  overload;
    constructor Create(FUsers : IDlUsers); overload;  { used for Dependency injection }
  end;

implementation

constructor TUserCtrl.Create;
begin
  Create(Dl_Users);
end;
constructor TUserCtrl.Create(FUsers : IDlUsers);
begin
  Self.FUsers          := FUsers;
end;
function TUserCtrl.LogonUser_VerifyPassword(UserID: Integer; Pwd: String): Boolean;
begin
   result := FUsers.VerifyPassword(UserId,Pwd);
end 
function  TUserCtrl.LogonUser_CheckPwd(UserID: Integer; Pwd: String): Boolean;
var ErrorCode : Integer; 
begin
   result := FUsers.CheckPwd(UserID,Pwd,ErrorCode);
   // do what needs to be done with ErrorCode
end;

///// Unit tests


procedure TestTDlUsers.SetUp;
begin
  inherited;
  FUsers    := TMock<IDlUsers>.Create;
  FUserCtrl := TUserCtrl.Create(FUsers);
end;
procedure TestTDlUsers.Test_LogonUser_VerifyPassword_GOOD_PASSWORD;
var Answer : Boolean;
begin
  FUsers.Setup.WillReturnDefault('VerifyPassword',False);
  FUsers.Setup.WillReturn(True).When.VerifyPassword(TEST_USERID,TEST_PASSWORD);
  Answer := FUserCtrl.LogonUser_VerifyPassword(TEST_USERID,TEST_PASSWORD,ErrorCode,ErrorMsg);
  CheckEquals(True,Answer);
end;


procedure TestTDlUsers.Test_LogonUser_CheckPwd_GOOD_PASSWORD;
var Answer : Boolean;
begin
  FUsers.Setup.WillReturnDefault('CheckPwd',False);

  // MAJOR Problem with line  CheckPwd  has an Out pramater 
  FUsers.Setup.WillReturn(True).When.CheckPwd(TEST_USERID,TEST_PASSWORD, ERROR_CODE);


  Answer := FUserCtrl.LogonUser_CheckPwd(TEST_USERID,TEST_PASSWORD,ErrorCode,ErrorMsg);
  CheckEquals(True,Answer);
end;
Was it helpful?

Solution

AFAIK this is a limitation of the TVirtualInterface standard class implementation, on which Delphi mocks relies. This is one of the weaknesses/limitations of the "new RTTI".

The only possible solution is to use a stubbing/mocking library which does not use this TVirtualInterface class.

The only library I know which has its own "virtual class" factory, is our Open Source mORMot framework (for Delphi 6 up to XE4, under Win32 and Win64). It supports var and out value parameters. For testing any out parameter value, you can use the ExpectsTrace() method - thanks to the great "Call Tracing" feature of mORMot.

OTHER TIPS

New delphi-mock can do this using reference-to-function-WillExecute:

FUsers.Setup.WillExecute('CheckPwd',
  // function CheckPwd(UserId: integer; Pwd: string; out ErrorCode: Integer; out ErrorMsg: String) : Boolean;
  function (const args : TArray<TValue>; const ReturnType : TRttiType) : TValue
  var
     aErrorCode: Integer;
     aErrorMsg: String;
     aResult: Boolean
  begin
    // check against 5, as arg[0] is tkInterface and the method parameters start with arg[1]
    Assert.AreEqual(5, Length(args), 'wrong number of arguments passed to IDlUsers.CheckPwd(UserId: integer; Pwd: string; out ErrorCode: Integer; out ErrorMsg:String) : Boolean');

    Assert.IsTrue(args[1].IsType<integer>, 'wrong argument 1 type passed to IDlUsers.CheckPwd(UserId: integer; Pwd: string; out ErrorCode: Integer; out ErrorMsg:String) : Boolean');
    Assert.IsTrue(args[2].IsType<string>, 'wrong argument 2 type passed to IDlUsers.CheckPwd(UserId: integer; Pwd: string; out ErrorCode: Integer; out ErrorMsg:String) : Boolean');
    Assert.IsTrue(args[3].IsType<Integer>, 'wrong argument 3 type passed to IDlUsers.CheckPwd(UserId: integer; Pwd: string; out ErrorCode: Integer; out ErrorMsg:String) : Boolean');
    Assert.IsTrue(args[4].IsType<string>, 'wrong argument 4 type passed to IDlUsers.CheckPwd(UserId: integer; Pwd: string; out ErrorCode: Integer; out ErrorMsg:String) : Boolean');

    // ...

    arg[3] := TValue.From<Integer>(aErrorCode);
    arg[4] := TValue.From<string>(aErrorMsg);
    Result := TValue.From<Boolean>(aResult);
  end);

(I use Delphi XE7 and DUnitX)

The library you're using gives no consideration for parameters passed by reference. It treats all parameters as input values to be matched on a call to the mock. Furthermore, it has no way to assign new values to such parameters when the mocked function is called.

An alternative is for you to change the interface for your function to make it testable. You could throw an exception on failure. However, exceptions aren't really appropriate for this function since failure to enter the correct user name and password isn't an exceptional event. Instead, consider returning the error code. Reserve one code (typically zero) to indicate success.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top