How to trigger an event on a Mock?

29 views
Skip to first unread message

NewDev

unread,
Apr 25, 2024, 9:50:19 AMApr 25
to Spring4D
Hi everyone, I'm trying to raise an event on a mock to test the behaviour of the class where the mock is injected. This is the kind of code I have:

type
  TMyEvent = procedure(Sender: TObject; Data: string) of object;

  IService = interface(IInvokable)
    function GetOnMyEvent: TMyEvent;
    procedure SetOnMyEvent(const Value: TMyEvent);
    property OnMyEvent: TMyEvent read GetOnMyEvent write SetOnMyEvent;
  end;

  TLogic = class
  private
    fService: IService;
    fComputedData: string;
    procedure ServiceEventHandler(Sender: TObject; Data: string);
  public
    property ComputedData: string read fComputedData;
    constructor Create(Service: IService);
  end;

constructor TLogic.Create(Service: IService);
begin
  fService := Service;
  fService.OnMyEvent := ServiceEventHandler;
end;

procedure TLogic.ServiceEventHandler(Sender: TObject; Data: string);
begin
  // Computed Data is updated here
end;


In my test I'd like something like this pseudo code:

  fServiceMock.Setup.Returns<TMyEvent>(
    // The raised event passing the value for Data
  ).When.OnGetMyEvent;


I'm used to this way of testing because I normally do this in C# with NSubstitute. Being pretty new to Delphi I'm not certain the same thing can be done, that is raising the event on the mock and passing a certain value for Data as argument.

Is there a way to achieve this? What should I change in my code to follow this kind of testing pattern?

Stefan Glienke

unread,
Apr 25, 2024, 11:55:04 AMApr 25
to Spring4D
Interesting question. Which behavior do you expect? That it returns what you assigned to it via the setter of OnMyEvent or something else?

Because there is no RTTI for properties in interfaces there is no built-in mechanism using that to connect the getter and setter and make them behave like in NSubstitute and most likely many other C# mocking libraries.
That means we have to wire them up manually (I will think about a way to do that is nicer and less convoluted and error-prone).

Anyhow for the time being here is how you would set it up:

  serviceMock.Setup.Executes(
    function(const call: TCallInfo): TValue
    var
      e: TMyEvent;
    begin
      e := call[0].AsType<TMyEvent>();
      e := serviceMock.Setup.Returns<TMyEvent>(e).When.OnMyEvent;
    end).When.OnMyEvent := Arg.IsAny<TMyEvent>();


The second assignment to e is just to make the compiler accept what is simulating the call to the getter - the value it got from the call parameter was already passed to Returns.
That means every time the setter is being called it resets what the getter will return.

Another way to write this would be even more convoluted and that would be the way I would internally solve this when coming up with a nicer syntax.
This will avoid resetting the behavior of the getter and instead use the captured variable to store the property value:

var
  evt: TValue;
  e: TMyEvent;
begin
  serviceMock.Setup.Executes(
    function(const call: TCallInfo): TValue
    begin
      evt := call[0];
    end).When.OnMyEvent := Arg.IsAny<TMyEvent>();
  e := serviceMock.Setup.Executes(
    function(const call: TCallInfo): TValue
    begin
      Result := evt;
    end).When.OnMyEvent;


We are using the evt variable to serve as the property field. The e variable is still needed for the getter call.

NewDev

unread,
Apr 25, 2024, 5:33:54 PMApr 25
to Spring4D
Hi Stefan, the behaviour I expect would be that given a call like this:

fServiceMock.Instance.OnMyEvent(nil, 'A');

the class under test would receive 'A' as Data in the corresponding event handler.

Since I'm not as skilled in Delphi, I think the best way to describe what I'm aiming for is to show the equivalent test code in C# with NSubstitute:

ServiceMock.OnMyEvent += Raise.EventWith(ServiceMock, new MyEventArgs { Data = "A" });
ServiceMock.OnMyEvent += Raise.EventWith(ServiceMock, new MyEventArgs { Data = "B" });
ServiceMock.OnMyEvent += Raise.EventWith(ServiceMock, new MyEventArgs { Data = "C" });

Assert.AreEqual("ABC", Logic.ComputedData);


I'm looking at your code and at the moment I find a little bit difficult to understand, but as much as it works for my intended goal it'd be fine, I've got time to learn better...

Stefan Glienke

unread,
Apr 25, 2024, 6:01:13 PMApr 25
to Spring4D
Yes, for that to work you need to set up the setter and getter for the OnMyEvent property which the code I posted does.

There is nothing like the Raise.EventWith but if I understand that right this is not needed in Delphi because you can invoke an event from the outside unlike in C#.

If I had to guess I would say NSubstitute auto mocks properties to work as it does according to their doc: https://nsubstitute.github.io/help/set-return-value/#for-properties

In other mocking libraries such as Moq, you explicitly have to set up every property and you can do that in various ways - see https://github.com/devlooped/moq/wiki/Quickstart#properties

Since there is no RTTI for properties on interfaces the auto mocking is not possible as is anything remotely similar to the syntax that NSubstitute does 
because they are using generic extension methods and expressions that enable their fluent API - both features that Delphi does not have.

I opted for a more moq-like design before and likely have something similar for the properties. However, the syntax will have to call both the getter and 
the setter and then internally they can be matched and connected to work as a property where it returns the value you previously set.

The following code is just an idea:

ServiceMock.SetupProperty.OnMyEvent := ServiceMock.Setup.Returns(<optional default value>).When.OnMyEvent;

or for a possibly shorter syntax (risking all the hate for using the keyword that shall not be used):

with ServiceMock.SetupProperty(<optional default value>) do OnMyEvent := OnMyEvent;

Both are a bit weird with assigning the property to itself but as I said the getter and setter have to be called to run to be set up.
As I said these are just some ideas going through my head syntax-wise - I have to check how to implement them internally.
Reply all
Reply to author
Forward
0 new messages