One of the objectively coolest features in Rubberduck is the Fakes API. Code that pops a MsgBox
for example, needs a way to work without actually popping that message box, otherwise that code cannot be unit tested… without somehow hijacking the MsgBox
function. The Fakes API does exactly that: it hooks into the VBA runtime, intercepts specific internal function calls, and makes it return exactly what your test setup …set up.
This API can stop time, or Now
can be told to return 1:59AM on first invocation, 1:00AM on the next, and then we can test and assert that some time-sensitive logic survives a daylight savings time toggle, or how Timer
-dependent code behaves at midnight.
Let’s take a look at the members of the IFakesProvider
interface.
Fakes Provider
Fakes for many of the internal VBA standard library functions exist since the initial release of the feature, although some providers wouldn’t always play nicely together – thanks to a recent pull request from @tommy9 these issues have been resolved, and a merry bunch of additional implementations are now available in pre-release builds:
Name | Description | Parameter names |
---|---|---|
MsgBox | Configures VBA.Interaction.MsgBox calls | Fakes.Params.MsgBox |
InputBox | Configures VBA.Interaction.InputBox calls | Fakes.Params.InputBox |
Beep | Configures VBA.Interaction.Beep calls | |
Environ | Configures VBA.Interaction.Environ calls | Fakes.Params.Environ |
Timer | Configures VBA.DateTime.Timer calls | |
DoEvents | Configures VBA.Interaction.DoEvents calls | |
Shell | Configures VBA.Interaction.Shell calls | Fakes.Params.Shell |
SendKeys | Configures VBA.Interaction.SendKeys calls | Fakes.Params.SendKeys |
Kill | Configures VBA.FileSystem.Kill calls | Fakes.Params.Kill |
MkDir | Configures VBA.FileSystem.MkDir calls | Fakes.Params.MkDir |
RmDir | Configures VBA.FileSystem.RmDir calls | Fakes.Params.RmDir |
ChDir | Configures VBA.FileSystem.ChDir calls | Fakes.Params.ChDir |
ChDrive | Configures VBA.FileSystem.ChDrive calls | Fakes.Params.ChDrive |
CurDir | Configures VBA.FileSystem.CurDir calls | Fakes.Params.CurDir |
Now | Configures VBA.DateTime.Now calls | |
Time | Configures VBA.DateTime.Time calls | |
Date | Configures VBA.DateTime.Date calls | |
Rnd * | Configures VBA.Math.Rnd calls | Fakes.Params.Rnd |
DeleteSetting * | Configures VBA.Interaction.DeleteSetting calls | Fakes.Params.DeleteSetting |
SaveSetting * | Configures VBA.Interaction.SaveSetting calls | Fakes.Params.SaveSetting |
Randomize * | Configures VBA.Math.Randomize calls | Fakes.Params.Randomize |
GetAllSettings * | Configures VBA.Interaction.GetAllSettings calls | |
SetAttr * | Configures VBA.FileSystem.SetAttr calls | Fakes.Params.SetAttr |
GetAttr * | Configures VBA.FileSystem.GetAttr calls | Fakes.Params.GetAttr |
FileLen * | Configures VBA.FileSystem.FileLen calls | Fakes.Params.FileLen |
FileDateTime * | Configures VBA.FileSystem.FileDateTime calls | Fakes.Params.FileDateTime |
FreeFile * | Configures VBA.FileSystem.FreeFile calls | Fakes.Params.FreeFile |
IMEStatus * | Configures VBA.Information.IMEStatus calls | |
Dir * | Configures VBA.FileSystem.Dir calls | Fakes.Params.Dir |
FileCopy * | Configures VBA.FileSystem.FileCopy calls | Fakes.Params.FileCopy |
Parameter Names
The IVerify.ParameterXyz
members make a unit test fail if the specified parameter wasn’t given a specified value, but the parameter names must be passed as strings. This is a UX issue: the API essentially requires hard-coded magic string literals in its users’ code; this is obviously error-prone and feels a bit arcane to use. The IFakesProvider
interface has been given a Params
property that gets an instance of a class that exposes the parameter names for each of the IFake
implementations, as shown in the list above, and the screenshot below:

Note: the PR for this feature has not yet been merged at the time of this writing.
Testing Without Fakes (aka Testing with Stubs)
Unit tests have a 3-part structure: first we arrange the test, then we act by invoking the method we want to test; lastly, we assert that an actual result matches the expectations. When using fakes, we configure them in the arrange part of the test, and in the assert part we can verify whether (and/or how many times) a particular method was invoked with a particular parameterization.
Let’s say we had a procedure we wanted to write some tests for:
Public Sub TestMe()
If MsgBox("Print random number?", vbYesNo + vbQuestion, "Test") = vbYes Then
Debug.Print Now & vbTab & Rnd * 42
Else
Debug.Print Now
End If
End Sub
If we wanted to make this logic fully testable without the Fakes
API, we would need to inject (likely as parameters) abstractions for MsgBox
, Now
, and Debug
dependencies: instead of invoking MsgBox
directly, the procedure would be invoking the Prompt
method of an interface/class that wraps the MsgBox
functionality. Unit tests would need a stub implementation of that interface in order to allow some level of configuration setup – an invocation counter, for example. A fully testable version of the above code might then look like this:
Public Sub TestMe(ByVal MessageBox As IMsgBox, ByVal Random As IRnd, ByVal DateTime As IDateTime, ByVal Logger As ILogger)
If MessageBox.Prompt("Print random number?", "Test") = vbYes Then
Logger.LogDebug DateTime.Now & vbTab & Random.Next * 42
Else
Logger.LogDebug DateTime.Now
End If
End Sub
The method is testable, because the caller controls all the dependencies. We’re probably injecting an IMsgBox
that pops a MsgBox
, an IRnd
that wraps Rnd
, a DateTime
parameter that returns VBA.DateTime.Now
and an ILogger
that writes to the debug pane, but we don’t know any of that. I fact, we could very well run this method with an ILogger
that writes to some log file or even to a database; the IRnd
implementation could consistently be returning 0.4 on every call, IDateTime.Now
could return Now
adjusted to UTC, and IMsgBox
might actually display a fancy custom modal UserForm
dialog – either way, TestMe
doesn’t need to change for any of that to happen: it does what it needs to do, in this case fetching the next random number and outputting it along with the current date/time if a user prompt is answered with a “Yes”, otherwise just output the current date/time. It’s the interfaces that provide the abstraction that’s necessary to decouple the dependencies from the logic we want to test. We could implement these interfaces with stubs that simply count the number of times each member is invoked, and the logic we’re testing would still hold.
We could then write tests that validate the conditional logic:
'@TestMethod
Public Sub TestMe_WhenPromptYes_GetsNextRandomValue()
' Arrange
Dim MsgBoxStub As StubMsgBox ' implements IMsgBox, but we want the stub functionality here
Set MsgBoxStub = New StubMsgBox
MsgBoxStub.Returns vbYes
Dim RndStub As StubRnd ' implements IRnd, but we want the stub functionality here too
Set RndStub = New StubRnd
' Act
Module1.TestMe MsgBoxStub, RndStub, New DateTimeStub, New LoggerStub
' Assert
Assert.Equals 1, RndStub.InvokeCount
End Sub
'@TestMethod
Public Sub TestMe_WhenPromptNo_DoesNotGetNextRandomValue()
' Arrange
Dim MsgBoxStub As StubMsgBox
Set MsgBoxStub = New StubMsgBox
MsgBoxStub.Returns vbNo
Dim RndStub As StubRnd
Set RndStub = New StubRnd
' Act
Module1.TestMe MsgBoxStub, RndStub, New DateTimeStub, New LoggerStub
' Assert
Assert.Equals 0, RndStub.InvokeCount
End Sub
These stub implementations are class modules that need to be written to support such tests. StubMsgBox
would implement IMsgBox
and expose a public Returns
method to configure its return value; StubRnd
would implement IRnd
and expose a public InvokeCount
property that returns the number of times the IRnd.Next
method was called. In other words, it’s quite a bit of boilerplate code that we’d usually rather not need to write.
Let’s see how using the Fakes API changes that.
Using Rubberduck.FakesProvider
The standard test module template defines Assert
and Fakes
private fields. When early-bound (needs a reference to the Rubberduck type library), the declarations and initialization look like this:
'@TestModule
Option Explicit
Option Private Module
Private Assert As Rubberduck.AssertClass
Private Fakes As Rubberduck.FakesProvider
'@ModuleInitialize
Public Sub ModuleInitialize()
Set Assert = CreateObject("Rubberduck.AssertClass")
Set Fakes = CreateObject("Rubberduck.FakesProvider")
End Sub
The Fakes API implements three of the four stubs for us, so we still need an implementation for ILogger
, but now the method remains fully testable even with direct MsgBox
, Now
and Rnd
calls:
Public Sub TestMe(ILogger Logger)
If MsgBox("Print random number?", vbYesNo + vbQuestion, "Test") = vbYes Then
Logger.LogDebug Now & vbTab & Rnd * 42
Else
Logger.LogDebug Now
End If
End Sub
With an ILogger
stub we could write a test that validates what’s being logged in each conditional branch (or we could decide that we don’t need an ILogger
interface and we’re fine with tests actually writing to the debug pane, and leave Debug.Print
statements in place), but let’s just stick with the same two tests we wrote above without the Fakes API. They look like this now:
'@TestMethod
Public Sub TestMe_WhenPromptYes_GetsNextRandomValue()
' Arrange
Fakes.MsgBox.Returns vbYes
' Act
Module1.TestMe New LoggerStub ' ILogger is irrelevant for this test
' Assert
Fakes.Rnd.Verify.Once
End Sub
'@TestMethod
Public Sub TestMe_WhenPromptNo_DoesNotGetNextRandomValue()
' Arrange
Fakes.MsgBox.Returns vbNo
' Act
Module1.TestMe New LoggerStub ' ILogger is irrelevant for this test
' Assert
Fakes.Rnd.Verify.Never
End Sub
We configure the MsgBox
fake to return the value we need, we invoke the method under test, and then we verify that the Rnd
fake was invoked once or never, depending on what we’re testing. A failed verification will fail the test the same as a failed Assert
call.
The fakes automatically track invocations, and remember what parameter values each invocation was made with. Setup can optionally supply an invocation number (1-based) to configure specific invocations, and verification can be made against specific invocation numbers as well, and we could have a failing test that validates whether Randomize
is invoked when Rnd
is called.
API Details
The IFake
interface exposes members for the setup/configuration of fakes:
Name | Description |
---|---|
AssignsByRef | Configures the fake such as an invocation assigns the specified value to the specified ByRef argument. |
Passthrough | Gets/sets whether invocations should pass through to the native call. |
RaisesError | Configures the fake such as an invocation raises the specified run-time error. |
Returns | Configures the fake such as the specified invocation returns the specified value. |
ReturnsWhen | Configures the fake such as the specified invocation returns the specified value given a specific parameter value. |
Verify | Gets an interface for verifying invocations performed during the test. See IVerify . |
The IVerify
interface exposes members for verifying what happened during the “Act” phase of the test:
Name | Description |
---|---|
AtLeast | Verifies that the faked procedure was called a specified minimum number of times. |
AtLeastOnce | Verifies that the faked procedure was called one or more times. |
AtMost | Verifies that the faked procedure was called a specified maximum number of times. |
AtMostOnce | Verifies that the faked procedure was not called or was only called once. |
Between | Verifies that the number of times the faked procedure was called falls within the supplied range. |
Exactly | Verifies that the faked procedure was called a specified number of times. |
Never | Verifies that the faked procedure was called exactly 0 times. |
Once | Verifies that the faked procedure was called exactly one time. |
Parameter | Verifies that the value of a given parameter to the faked procedure matches a specific value. |
ParameterInRange | Verifies that the value of a given parameter to the faked procedure falls within a specified range. |
ParameterIsPassed | Verifies that an optional parameter was passed to the faked procedure. The value is not evaluated. |
ParameterIsType | Verifies that the passed value of a given parameter was of a type that matches the given type name. |
There’s also an IStub
interface: it’s a subset of IFake
, without the Returns
setup methods. Thus, IStub
is used for faking Sub
procedures, and IFake
for Function
and Property
procedures.
When to Stub Standard Library Members
Members of VBA.FileSystem
not covered include EOF
and LOF
functions, Loc
, Seek
, and Reset
. VBA I/O keywords Name
, Open
, and Close
operate at a lower level than the standard library and aren’t covered, either. VBA.Interaction.CreateObject
and VBA.Interaction.GetObject
, VBA.Interaction.AppActivate
, VBA.Interaction.CallByName
, and the hidden VBA.Interaction.MacScript
function, aren’t implemented.
Perhaps CreateObject
and GetObject
calls belong behind an abstract factory and a provider interface, respectively, and perhaps CallByName
doesn’t really need hooking anyway. In any case there are a number of file I/O operations that cannot be faked and demand an abstraction layer between the I/O work and the code that commands it: that’s when you’re going to want to write stub implementations.
If you’re writing a macro that makes an HTTP request and processes its response, consider abstracting the HttpClient
stuff behind an interface (something like Function HttpGet(ByVal Url As String)
): the macro code will gain in readability and focus, and then if you inject that interface as a parameter, then a unit test can inject a stub implementation for it, and you can write tests that handle (or not?) an HTTP client error, or process such or such JSON or HTML payload – without hitting any actual network and making any actual HTTP requests.
Until we can do mocking with Rubberduck, writing test stubs for our system-boundary interfaces is going to have to be it. Mocking would remove the need to explicitly implement most test stubs, by enabling the same kind of customization as with fakes, but with your own interfaces/classes. Or Excel’s. Or anything, in theory.