4

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?

Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
user1114
  • 1,071
  • 2
  • 15
  • 33

1 Answers1

7

tldr; No

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

Broken down:

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

What this means in relation to your question:

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.

Comintern
  • 21,855
  • 5
  • 33
  • 80