Search code examples
delphidelphi-xe2rttidelphi-mocks

EInvalidCast wih mock function returning a pointer type


I've written an interface to wrap the Windows Threadpool API and many of those functions return plain Pointer types.

Now I'm writing tests and want to use the delphi-mocks framework to mock that wrapper interface.

The problem is that the TMock setup interface takes a TValue object to specify a default return value for the mocked functions and I can't see a way how to do that correctly from the available TValue functions. Though I've seen that ekPointer is a valid TTypeKind value.

When the mocked function is called I receive an EInvalidCast exception from the corresponding RTTI invocation.
That happens specifically, when the RTTI invocation tries to cast the return type value from the implicit TValue object.

My code looks roughly like this1:

type
    PTP_POOL = Pointer;
    PTP_CLEANUP_GROUP = Pointer;

    IThreadPoolApi = interface(IInterface)
        {...}
        function CreateThreadPool() : PTP_POOL;
        function CreateThreadpoolCleanupGroup() : PTP_CLEANUP_GROUP;
        {...}
    end;

the class under test

type
    TThreadPool = class(TInterfacedObject, IThreadPool)
        FTPApi : IThreadPoolApi;
        FPTPPool : PTP_POOL;
        FPTPCleanupGroup : PTP_CLEANUP_GROUP;
        {...}
    public
        constructor Create(iapi : IThreadPoolApi);
    end;

implementation
constructor TThreadPool.Create(iapi : IThreadPoolApi);
begin
    inherited Create();
    FTPApi := iapi;

    {**** Here I get a EInvalidCast exception when mocking the interface ****}
    FPTPPool := FTPApi.CreateThreadPool();
    if(not assigned(FPTPPool)) then
    begin
        {Raise exception}
        raise EThreadPoolError('Cannot create TP thread pool');
    end;

    {**** This case should be tested ****}
    FPTPCleanupGroup := FTPApi.CreateThreadpoolCleanupGroup();
    if(not assigned(FPTPPool)) then
    begin
         {Raise exception}
         raise EThreadPoolError('Cannot create TP cleanup group');
    end;

    {...}
end;

and the testing stuff

procedure ThreadPoolTest.TestFail_CreateThreadpoolCleanupGroup();
var
   apiMock : TMock<IThreadPoolApi>;
   testproc : TTestLocalMethod;
begin
   apiMock := TMock<IThreadPoolApi>Create();
   {**** Needed to reach the call of CreateThreadpoolCleanupGroup
    but EInvalidCast is raised ****}
   apiMock.Setup.WillReturnDefault('CreateThreadPool',PTP_POOL($FFFFFFFF));
   {**** The case to be tested ****}
   apiMock.Setup.WillExecute('CreateThreadpoolCleanupGroup',
       function (const args : TArray<TValue>; const ReturnType : TRttiType) 
       : TValue
       begin
           result := nil;
       end);

    testproc := 
       procedure() 
       var
           threadpool : IThreadPool;
       begin
           threadpool := TThreadPool.Create(apiMock);
       end;
    DUnitX.Assert.WillRaise(testproc,EThreadPoolError,
                            'Cannot create TP cleanup group');
end;

TL;DR;

So the question is:
What do I need to do that TValue is created properly to contain a PTP_POOL pointer type?


1)It's a bit too much code to setup a MCVE, so I just sketch it here to give you the background, see the {**** highlighted comments ****}


Solution

  • When assigning a raw pointer to a TValue, use the TValue.From<T>() method, eg:

    TValue.From<PTP_POOL>(nil);
    

    Or, if you want to return a literal value:

    TValue.From<PTP_POOL>(PTP_POOL(value));
    

    TValue does not have an implicit conversion for raw pointers, but it does have implicit conversions for TObject and TClass pointers.

    If you assign an untyped nil pointer directly to TValue, it calls TValue.Implicit(TObject), which will return the TValue.Empty property when passed a nil pointer.

    If you assign a typed non-TObject pointer directly to TValue, it calls TValue.Implicit(TClass), which returns TValue.Empty if the pointer is nil, otherwise it returns a TValue of type TClass with the value of the pointer, even if it is not actually pointing at a valid class type.

    If you pass a pointer to TValue.From<T>(), it returns a TValue of type T with the value of the pointer, even if it is nil.

    This difference is subtle, but very important.