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
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:
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
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
IRnd that wraps
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.
The standard test module template defines
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
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
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
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.
IFake interface exposes members for the setup/configuration of fakes:
|AssignsByRef||Configures the fake such as an invocation assigns the specified value to the specified |
|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 interface exposes members for verifying what happened during the “Act” phase of the test:
|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
When to Stub Standard Library Members
VBA.FileSystem not covered include
Reset. VBA I/O keywords
Close operate at a lower level than the standard library and aren’t covered, either.
VBA.Interaction.CallByName, and the hidden
VBA.Interaction.MacScript function, aren’t implemented.
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.