rubberduck-vba / Rubberduck

Every programmer needs a rubberduck. COM add-in for the VBA & VB6 IDE (VBE).
https://rubberduckvba.com
GNU General Public License v3.0
1.91k stars 299 forks source link

UnitTesting Events #5577

Open sancarn opened 4 years ago

sancarn commented 4 years ago

Hey all,

Has rubberduck got a way of unit testing class event handler calls?

E.G.

Dim withEvents obj as SomeClass
set obj = new SomeClass
set objTracker = Fakes.trackEvents(obj)
obj.doSomething()
Debug.Print objTracker.called("beforeDoSomething")
Debug.Print objTracker.called("afterDoSomething")

If so, does anyone know where is this implemented in the source? 😛

If not what's the work around? Should we be creating a class and tracking this more directly?

Dim WithEvents obj as new SomeObject
Dim called as new collection

Sub obj_beforeDoSomething()
   called.add "beforeDoSomething"
End Sub
Sub obj_afterDoSomething()
   called.add "beforeDoSomething"   
End Sub
Greedquest commented 3 years ago

This is something I've been after for a while too. I don't know if C# allows you to hook onto arbitrary COM events - e.g. does IDispatch expose VBA Class events in a way that lets C# enumerate and hook into them, so you can set up some EventLogger(source As IDispatch). I know VBA doesn't have any easy way of enumerating Events a class exposes, let alone adding handlers to them at runtime. If possible in C#, then a class that logs event args and timestamps / calling order would probably be sufficient and a great addition to the RD testing suite.

If not then an alternative could be a feature that uses RD's existing knowledge of the users codebase and the events each of their classes exposes, and then creates a VBA handler manually and adds it to the project. Some parameterised Template class which takes in a base object, inspects its events in the RD parse tree, and stringly creates some handler code. I could suggest a template structure for this?

Either way I think it'd be so useful to have this feature, I use Events extensively for Asynchronous style programming and testing these without having to write custom listeners for events would be delightful

bclothier commented 3 years ago

I see two possible solutions:

1) create a class module implementing the events with a WithEvents and use it as a SUT, executing Verify within the events of the class module, and having your test module call it.

2) Using type library API to create a connection point to the source interface representing the events. That requires a mock object, for which we need a mocking framework.

retailcoder commented 3 years ago

I could have sworn I had written about "testing that an event was raised" in "how to unit test VBA code", ...but I can't find any mention of it (might be I removed it at one point).

The way it was setup felt a bit clunky (that's probably why I ended editing it out), but basically when your SUT is an event provider then you can have a Private WithEvents SUT As Something, and then some WasSomeEventRaised As Boolean state flag that your SUT_SomeEvent handler sets to True (and that your @TestCleanUp method sets back to False), and then the test can Assert.IsTrue Test.WasSomeEventRaised.

Now the problem is that RD test modules being standard modules, you cannot have a Private WithEvents SUT variable in the test module, so you'd need to have some "test event handler" class that can handle the SUT events and expose the invoke state to the tests.

That's basically what @bclothier is suggesting as solution 1 =)

Greedquest commented 3 years ago

@retailcoder yes, and that's what I was suggesting as some stringly generated template class - RD could do all the heavy lifting of generating source code for the Boolean toggling, event handling test classes

Or indeed instead of true/false, why not count events / store them as entries in a collection which captures the state of the parameters too, alongside a timestamp to order them. I wanted to test a buffered list that raised an event every n items for example, so Assert.AreEqual 2, Test.CountOfSomeEvent would have been ideal

Is the typelib API approach (2) the proposed method to "listen" to VBA COM events directly within C# then? Just out of interest:)

sancarn commented 3 years ago

@retailcoder Yes I had a suspission this was the current methodology.

Using type library API to create a connection point to the source interface representing the events. That requires a mock object, for which we need a mocking framework.

@bclothier This is what I had hoped for! Does the type library expose connection points? 😲 I had looked into it myself briefly but couldn't see where abouts connection points would be stored?

bclothier commented 3 years ago

No, we need the type library API in order to obtain the definition of source interface from VBA's class that sources events. The key is that we then use the IID (the GUID of the interface) to then create a connection point. See: IConnectionPointContainer::FindConnectionPoint and also SafeEventedComWrapper as an example of how the connection points are used. The key is that you have to know the interface's IID and also have a object that implements that interface in order to handle events. The trick, however, is doing it at runtime, without knowing what the interface's shape is like until then and somehow implement it. For that reason, the C# object may need to implement the IReflect.