Search code examples
vbarubberduck

Can RubberDuck test if a program ends?


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?


Solution

  • 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.