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.


































