-
Notifications
You must be signed in to change notification settings - Fork 309
VBA Moq Mocking Framework
Since v2.5.92.6342, Rubberduck has a very unique new feature: the popular mocking framework Moq was essentially wrapped with a COM API that can be invoked from Rubberduck unit tests to create and configure mocks for any COM object (with some caveats).
While user code is compiled into COM objects and the framework will allow user-defined objects and interfaces to be mocked, a technical limitation with the underlying library that generates the proxy types at run-time makes it hard to work with user objects, because these objects will only be loaded once in the managed memory space, so modifying the code and recompiling will not load the updated object types on the .net side - in other words the entire host process must be restarted in order for user code changes to be taken into account.
You may be familiar with the concept of test stubs, where you create a fake implementation of an interface solely for tests to inject in place of whatever dependencies the code under test might have. Working with these stubs is tedious, and stubbing something complex like a worksheet can easily become a bottomless rabbit hole. Stubs are a great tool, but mocks are a whole other level.
With mocking, there is no concrete implementation: instead, the framework uses your instructions to create and configure a stub implementation of literally any object. This is mind-blowing black magic stuff: with just a few lines of code, you can mock Excel.Application
and completely control every single one of its members.
Curious? Let's jump right into it.
At the top of the API we find the Rubberduck.MockProvider
class, which will now get automatically initialized in any new test module you add to your VBA projects. The class you'll use to configure mocks is Rubberduck.ComMock
, and you will always get one from the provider, like so (early bound for clarity):
Option Explicit
'Private Assert As Rubberduck.AssertClass
'Private Fakes As Rubberduck.FakesProvider
Private Mocks As Rubberduck.MockProvider
'@ModuleInitialize
Private Sub ModuleInitialize()
'this method runs once per module.
'Set Assert = New Rubberduck.AssertClass
'Set Fakes = New Rubberduck.FakesProvider
Set Mocks = New Rubberduck.MockProvider
End Sub
'@TestMethod("Uncategorized")
Public Sub TestMethod1()
'arrange
Dim Mock As Rubberduck.ComMock
Set Mock = Mocks.Mock("Excel.Application") 'here we create a new mock using the Excel.Application progid
'then we configure our mock as per our needs...
Mock.SetupWithReturns "Name", "Mocked-Excel"
Mock.SetupWithCallback "CalculateFull", AddressOf OnAppCalculate
Dim Mocked As Excel.Application
Set Mocked = Mock.Object 'ComMock.Object represents the mocked object and always implements the COM interface it's mocking
'act
'just making sure the mock works :)
Debug.Print Mocked.Name
Mocked.CalculateFull
'it would normally look more like this: we inject the mocked dependency into the object we want to test in isolation.
'With SomeMacro.Create(Mocked)
' .Execute
'End With
'assert
'use the ComMock.Verify method to fail the test if a method that was setup was not invoked as per the test's specifications:
Mock.Verify "CalculateFull", Mocks.Times.AtLeastOnce
End Sub
Public Sub OnAppCalculate()
'we can use AddressOf to implement callbacks that the mock will invoke instead of the concrete method:
Debug.Print "A full recalc was made by the mocked Excel.Application instance."
End Sub
As you can see it's a very similar principle as the Fakes API, except we're not hooking any libraries here, no: instead we're spawning a .net type that implements Excel.Application on the fly, telling it what to do when such or such member is invoked, and then we can validate that things actually happened as expected.
Let's dig deeper now.
The top-level object for the mocking API has the following members:
This method creates and returns a new mock for a specified interface or progID (or GUID) string.
This get-only property exposes the Times
methods, which are used together with the ComMock.Verify
method to specify how a test should fail.
This get-only property exposes the SetupArgumentCreator
API, which allows for many flexible ways to specify exactly how a mocked method is supposed to be invoked by the code under test:
-
Is(Value)
sets up an argument that must be the specified value -
IsAny()
sets up an argument that does not need to have any particular value -
IsIn(Values())
sets up an argument that must be one of the specified values -
IsInRange(Start, End, SetupArgumentRange)
sets up an argument that must be within a specified range of values, either inclusively or exclusively -
IsNotIn(Values())
sets up an argument that must not be any of the specified values -
IsNotNull()
sets up an argument that must be any non-null (Nothing
) object reference
An object of this type is returned by the MockProvider.Mock
function, which serves as a factory method for creating mocks. A COM mock has the following members:
A get-only property that holds a reference to the mocked COM object; this reference is exposed as Object
, but can always safely be cast to the mocked interface type.
These two properties identify the mocked interface type in a way that Rubberduck can use to retrieve the type definitions.
Use this method to configure a member call on the mock, that does not return a value (i.e., a Sub
or Property Let
/Set
procedure). This function returns the configured mock, so multiple setup calls can be chained in the same instruction.
Use MockProvider.It
to specify the Args
of Setup
and Verify
methods.
Use this method to configure a member call on the mock, that returns an object reference; the returned object will also be mocked. This function also returns the configured mock, so multiple setup calls can be chained in the same instruction.
Use this method with the AddressOf
operator to specify a callback to invoke when the configured member call is invoked on the mock. The callback must be a public method defined in a standard module.
Use this method to configure a member call on the mock so that it returns a specific value. This method does not return the configured mock and thus cannot be chained with other setup calls, at least not in the initial version.
Fails the test if a specific invocation was not performed on the mock object.
rubberduckvba.com
© 2014-2025 Rubberduck project contributors
- Contributing
- Build process
- Version bump
- Architecture Overview
- IoC Container
- Parser State
- The Parsing Process
- How to view parse tree
- UI Design Guidelines
- Strategies for managing COM object lifetime and release
- COM Registration
- Internal Codebase Analysis
- Projects & Workflow
- Adding other Host Applications
- Inspections XML-Doc
-
VBE Events