I'm developing tests with RubberDuck, and would like to test MsgBox outputs from a program. The catch is that the program ends right after outputting the MsgBox - there's literally an "End" Statement.
When running a RubberDuck test and using Fakes.MsgBox.Returns, there's an inconclusive yellow result with message "Unexpected COM exception while running tests"
I've tried placing an "Assert.Fail" at the end of the test; however, it seems like the program ending throws things off.
Is it possible for a test in RubberDuck to detect if the program ends?
Rubberduck unit tests are executed in the context of the VBA runtime - that is, the VBA unit test code is being run from inside the host application. Testing results are reported back to Rubberduck via its API. If you look at the VBA code generated when you insert a test module, it gives a basic idea of the architecture of how the tests are run. Take for example this unit test from our integration test suite:
'HERE BE DRAGONS. Save your work in ALL open windows.
'@TestModule
'@Folder("Tests")
Private Assert As New Rubberduck.AssertClass
Private Fakes As New Rubberduck.FakesProvider
'@TestMethod
Public Sub InputBoxFakeWorks()
On Error GoTo TestFail
Dim userInput As String
With Fakes.InputBox
.Returns vbNullString, 1
.ReturnsWhen "Prompt", "Second", "User entry 2", 2
userInput = InputBox("First")
Assert.IsTrue userInput = vbNullString
userInput = InputBox("Second")
Assert.IsTrue userInput = "User entry 2"
End With
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
This creates a managed class that "listens" for Asserts in the code being tested and evaluates the condition for passing or failing the test.
Private Assert As New Rubberduck.AssertClass
The FakesProvider
is a utility object for setting the hooks in the VB runtime to "ignore" or "spoof" calls from inside the VB runtime to, say, the InputBox
function.
Since the Fakes
object is declared As New
, the With
block instantiates a FakesProvider
for the test. The InputBox
method of Fakes
This sets a hook on the rtcInputBox
function in vbe7.dll which redirects all traffic from VBA to that function to the Rubberduck implementation. This is now counting calls, tracking parameters passed, providing return values, etc.
With Fakes.InputBox
The Returns and ReturnsWhen calls are using the VBA held COM object to communicate the test setup of the faked calls to InputBox
. In this example, it configures the InputBox
object to return a vbNullString
for call one, and "User entry 2" when passed a Prompt
parameter of "Second"
for call number two.
.Returns vbNullString, 1
.ReturnsWhen "Prompt", "Second", "User entry 2", 2
This is where the AssertClass comes in. When you run unit tests from the Rubberduck UI, it determines the COM interface for the user's code. Then, it calls invokes the test method via that interface. Rubberduck then uses the AssertClass
to test runtime conditions. The IsTrue
method takes a Boolean
as a parameter (with an optional output message). So on the line of code below, VB evaluates the expression userInput = vbNullString
and passes the result as the parameter to IsTrue
. The Rubberduck IsTrue
implementation then sets the state of the unit test based on whether or not the parameter passed from VBA meets the condition of the AssertClass
method called.
Assert.IsTrue userInput = vbNullString
Note that in the breakdown of how the code above executes, everything is executing in the VBA environment. Rubberduck is providing VBA a "window" to report the results back via the AssertClass
object, and simply (for some values of "simply") providing hook service through the FakesProvider
object. VBA "owns" both of those objects - they are just provided through Rubberduck's COM provider.
When you use the End
statement in VBA, it forcibly terminates execution at that point. The Rubberduck COM objects are no longer actively referenced by the client (your test procedure), and it's undefined as to whether or not that decrements the reference count on the COM object. It's like yanking the plug from the wall. The only thing that Rubberduck can determine at this point is that the COM client has disconnected. In your case that manifests as a COM exception caught inside Rubberduck. Since Rubberduck has no way of knowing why the object it is providing has lost communication, it reports the result of the test as "Inconclusive" - it did not run to completion.
That said, the solution is to refactor your code to not use End
. Ever. Quoting the documentation linked above End
...
Terminates execution immediately. Never required by itself but may be placed anywhere in a procedure to end code execution, close files opened with the Open statement, and to clear variables.
This is nowhere near graceful, and if you have references to other COM objects (other than Rubberduck) there is no guarantee that they will be terminated reliably.
Full disclosure, I contribute to the Rubberduck project and authored some of the code described above. If you want to get a better understanding of how unit testing functions (and can read c#), the implementations of the COM providers can be found at this link.