How to unit test VBA code?

So Rubberduck lets you write unit tests for your VBA code. If you’re learning VBA, or if you’re a seasoned VBA developer but have never written a unit test before, this can sound like a crazy useless idea. I know, because before I started working on Rubberduck, that’s how I was seeing unit tests: annoying, redundant code that tells you nothing F5/debugging doesn’t already tell you.

Right? What’s the point anyway?

First, it changes how you think about code. Things like the Single Responsibility Principle start becoming freakishly important, and you begin to break that monolithic macro procedure into smaller, more focused chunks. Future you, or whoever inherits your code, will be extremely thankful for that.

But not all VBA code should be unit-tested. Let’s see why.

Know what NOT to test

All code has dependencies. Some of these dependencies we can’t really do without, and don’t really affect anything – global-scope functions in the VBA Standard Library, for example. Other dependencies affect global state, require user input (MsgBox, InputBox, forms, dialogs, etc.) or access external resources – like a database, the file system, …or a worksheet.

For the sake of this article, say you have a simple procedure like this:

Public Sub DoSomething()
    Dim conn As ADODB.Connection
    Set conn = New ADODB.Connection
    conn.ConnectionString = "{connection string}"
    conn.Open
    Dim rs As ADODB.Recordset
    Set rs = conn.Execute("SELECT * FROM dbo.SomeTable")
    Sheet1.Range("A1").CopyFromRecordset rs
    conn.Close
End Sub

The problematic dependencies are:

  • conn, an ADODB connection
  • rs, an ADODB recordset
  • Sheet1, an Excel worksheet

Is that procedure doomed and completely untestable? Well, as is, …pretty much: the only way to write a test for this procedure would be to actually run it, and verify that something was dumped into Sheet1. In fact, that’s pretty much automating F5-debug: it’s an integration test, not a unit test – it’s a test, but it’s validating that all components work together. It’s not useless, but that’s not a unit test.

Refactoring

The procedure needs to be parameterless, because it’s invoked from some button: so we have a major problem here – there’s no way to factor out the dependencies!

Or is there? What if we introduced a class, and moved the functionality into there?

Now we’d be looking at this:

Public Sub DoSomething()
    With New MyTestableMacro
        .Run
    End With
End Sub

At this point we tremendously increased the macro’s abstraction level and that’s awesome, but we didn’t really gain anything. Or did we? Now that we’ve decoupled the macro’s entry point from the implementation, we can pull out the dependencies and unit-test the MyTestableMacro class! But how do we do that?

Think in terms of concerns:

  • Pulling data from a database
  • Writing the data to a worksheet

Now think in terms of objects:

  • We need some data service responsible for pulling data from a database
  • We need some spreadsheet service responsible for writing data to a worksheet

The macro might look like this now:

Public Sub DoSomething()

    Dim dataService As MyDbDataService
    Set dataService = New MyDbDataService

    Dim spreadsheetService As Sheet1Service
    Set spreadsheetService = New Sheet1Service

    With New MyTestableMacro
        .Run dataService, spreadsheetService
    End With

End Sub

Now if we think of MyDbDataService as an interface, we could conceptualize it like this:

Option Explicit
'@Folder "Services.Abstract"
'@Interface IDataService

Public Function GetSomeTable() As Variant
End Function

And if we think of Sheet1Service as an interface, we could conceptualize it like this:

Option Explicit
'@Folder "Services.Abstract"
'@Interface IWorksheetService

Public Sub WriteAllData(ByRef data As Variant)
End Sub

Notice the interfaces don’t know or care about ADODB.Recordset: the last thing we want is to have that dependency in our way, so we’ll be passing a Variant array around instead of a recordset.

Now the Run method’s signature might look like this:

Public Sub Run(ByVal dataService As IDataService, ByVal wsService As IWorksheetService)

Notice it only knows about abstractions, not the concrete implementations. All that’s missing is to make MyDbDataService implement the IDataService interface, and Sheet1Service implement the IWorksheetService interface.

Option Explicit
Implements IDataService
'@Folder "Services.Concrete"

Private Function IDataService_GetSomeTable() As Variant
    Dim conn As ADODB.Connection
    Set conn = New ADODB.Connection
    conn.ConnectionString = "{connection string}"
    conn.Open
    Dim rs As ADODB.Recordset
    Set rs = conn.Execute("SELECT * FROM dbo.SomeTable")
    'dump the recordset onto a temp sheet:
    Dim tempSheet As Excel.Worksheet
    Set tempSheet = ThisWorkbook.Worksheets.Add
    tempSheet.Range("A1").CopyFromRecordset rs
    IDataService_GetSomeTable = tempSheet.UsedRange.Value '2D variant array
    conn.Close
    tempSheet.Delete
End Function

Stubbing the interfaces

So here’s where the magic begins: the macro will definitely be using the above implementation, but nothing forces a unit test to use it too. A unit test would be happy to use something like this:

Option Explicit
Implements IDataService
'@Folder "Services.Stubs"

Private Function IDataService_GetSomeTable() As Variant
    Dim result(1 To 50, 1 To 10) As Variant
    IDataService_GetSomeTable = result
End Function

Public Function GetSomeTable() As Variant
    GetSomeTable = IDataService_GetSomeTable
End Function

You could populate the array with some fake results, expose properties and methods to configure the stub in every way your tests require (depending on what logic needs to run against the data after it’s dumped onto the worksheet) – for this example though all we need is for the method to return a 2D variant array, and the above code satisfies that.

Then we need a stub for the IWorksheetService interface, too:

Option Explicit
Implements IWorksheetService
'@Folder "Services.Stubs"

Private written As Boolean
Private arrayPointer As Long

Private Sub IWorksheetService_WriteAllData(ByRef data As Variant)
    written = True
    arrayPointer = VarPtr(data)
End Function

Public Property Get DataWasWritten() As Boolean
    DataWasWritten = written
End Property

Public Property Get WrittenArrayPointer() As Long
    WrittenArrayPointer = arrayPointer
End Property

Writing the tests

That’s all our test needs for now. See where this is going? DoSomething is using concrete implementations of the service interfaces that actually do the work, and a unit test can look like this:

'@TestMethod
Public Sub GivenData_WritesToWorksheet()
    'Arrange
    Dim dataServiceStub As MyDataServiceStub
    Set dataServiceStub = New MyDataServiceStub
    Dim wsServiceStub As MyWorksheetServiceStub
    Set wsServiceStub = New MyWorksheetServiceStub

    'Act
    With New MyTestableMacro
        .Run dataServiceStub, wsServiceStub
    End With

    'Assert
    Assert.IsTrue wsServiceStub.DataWasWritten
End Sub

If MyTestableMacro.Run invokes IWorksheetService.WriteAllData, this test will pass.

One more:

'@TestMethod
Public Sub WorksheetServiceWorksOffDataFromDataService()
    'Arrange
    Dim dataServiceStub As MyDataServiceStub
    Set dataServiceStub = New MyDataServiceStub
    Dim expected As Long
    expected = VarPtr(dataServiceStub.GetSomeTable)

    Dim wsServiceStub As MyWorksheetServiceStub
    Set wsServiceStub = New MyWorksheetServiceStub

    'Act
    With New MyTestableMacro
        .Run dataServiceStub, wsServiceStub
    End With

    Dim actual As Long
    actual = wsServiceStub.WrittenArrayPointer

    'Assert
    Assert.AreEqual expected, actual
End Sub

If the worksheet service receives the exact same array that the data service returned, this test should pass.

That was a relatively trivial example – the overhead (5 classes, including 2 interfaces and 2 stub implementations) is probably not justified given the simplicity of the task at hand (pull data from a database, dump that data to a worksheet). But hopefully it illustrates a number of things:

  • How to pull dependencies out of the logic that needs to be tested.
  • How to abstract the dependencies as interfaces.
  • How to implement test stubs for these dependencies, and how stubs can expose members that aren’t on the interface, for the tests to consume.
  • How unit tests document what the code is supposed to be doing, through descriptive naming.
  • VBA code can be just as object-oriented as any other code, with full-blown polymorphism and dependency injection.

Next tutorial should be about MSForms.UserForm, how not to use it, and how to test code that needs to pop a dialog. I didn’t mention anything about Rubberduck’s Fakes framework here either, but know that if one of your dependencies is a MsgBox and you have different code paths depending on whether the user clicked [Ok] or [Cancel], you can use Rubberduck’s Fakes API to literally configure how the MsgBox statement is going to behave when it’s invoked by a Rubberduck test.

21 thoughts on “How to unit test VBA code?”

    1. Thing is, that implies having an Office install on a server, which isn’t a recommended setup. Then your code must be written in such a way that it’s impossible to bring up any kind of dialog (from the VBA code or the host application, or RD, or another add-in), otherwise you have to way to know your server is stalled and waiting for a non-existing user to click some OK button. But to trigger the tests you would need RD to have some kind of API that can invoke the test runner in the add-in instance that’s loaded in the VBE… which is quite a complex thing to do. I don’t think it’s possible ATM. In VB6 a test project would be its own executable so that could possibly work, but in VBA… not convinced. Having a test runner for VBA code is already a feat =)

      Like

  1. I’m still new to unit testing and I’m trying to follow your example by implementing your code. Right now, both of the sample tests provided in the article are failing, so I’m trying to figure out where I went wrong. Is there enough code in your example to successfully run the unit tests or was the code in this article more for demonstrating the concepts? I might be getting hung up on all the different classes implemented – in the conclusion, you mention that there are 5 classes, but I seem to count 7: MyTestableMacro, IWorksheetService, IDataService, MyDbDataService, Sheet1Service, MyDataServiceStub, MyWorksheetServiceStub.

    Like

    1. I wasn’t counting the MyTestableMacro, MyDbDataService and Sheet1Service classes as overhead; the “live” code needs these classes too! 😃
      What’s not working exactly? Are you getting an error? I need to upload an actual downloadable example workbook..

      Like

      1. I imagine I’m just getting mixed up as the code from each class evolves in the article.
        A downloadable example workbook would be great! =)

        Like

  2. If I understand correctly, a user form should completely own the information as supplied to it for presentation. If there are filtered aspects to that data it would seem that there would necessarily be logic in the code behind. How would that be tested? For instance suppose there is a combo box that can display a full list of selections or a couple of subsets of the full list. Each subset is select-able by the user via option buttons. If one of the subsets has no members then the ability to choose that option should be disabled.

    Like

    1. In such complex cases it’s definitely helpful to extract a class that holds the “model”, i.e. pull it out of the form itself, and into its own separate, dedicated class. Similar to WPF/MVVM’s “ViewModel”, you can have an “AvailableThings” property which is your current (filtered) list of available…things; with the filtering logic implemented outside of the form and merely assigning collection properties, it’s now easy to test whether your filtering logic works. The option buttons just wire-up to this “ViewModel” by handling their respective “Change” event and invoking the appropriate filtering methods on the “ViewModel” class; the class can raise its own events, that the view can handle – e.g. changing the “AvailableThings” property could raise a “AvailableThingsChanged” event that the view could handle in some private “model_AvailableThingsChanged” procedure, which would then be responsible for updating the actual list of available things. This is a bit hard to describe in a little comment box, I hope I’m making sense 🙂

      Like

  3. I just completed my first really “usable” (and portable) class to clean text of multiple CrLf characters and other non-printing characters using TDD. It is a very simple class especially for y’all that have been doing that as a matter of daily programming, but for me it is a huge milestone. I learned a great deal from the simple exercise and the way Rubberduck makes it easy to do.
    Thanks for all that the RD community has done and continues to do!

    Liked by 1 person

  4. If a worksheet table is being used to house data and the technique to get the date is the one you show in “WorkbooProxyExample” is there any reasonable way to test that the data is being read properly using the ideas presented above? Is it really something that should be done? Is it actually reasonable to test the I/O to the data sheet somehow to make sure the data is getting where it is supposed to?

    Like

    1. Depends how deep you want to go down that rabbit hole: the main idea behind the worksheet proxy is to decouple the code from the worksheet – if you have an interface that says “I’m taking this data and persisting it onto a worksheet” the rest of your code doesn’t really need to know what’s really happening, nor whether the implementation is really writing to a real worksheet; in unit testing, there’s a point beyond which you just trust that the framework/library is doing its job, and I/O is off-limits (though, “integration testing” could do that).
      There’s a mocking framework coming soon that will allow mocking any Excel interface and write these tests easily – until then I would keep the spreadsheet work behind interfaces, and treat these interfaces as the “lowest level” dependencies that make the boundaries between the testable app code and the thin wrapper logic that interacts with actual worksheets and isn’t really testable. Imagine you have an interface that says “I’m writing to a file” or “I’m reading from a database” – you can’t really write tests for these components without incurring I/O and introducing elements into the test that are beyond what can be controlled by that test, like network/disk latency, authentication and authorization issues, file locks, etc.; it’s good to test these things at one point, but a test that does it isn’t really a “unit test” anymore.

      Like

  5. That is what I suspected. It’s “at the edge” and really doesn’t lend itself to testing per se. It either works or it doesn’t… i.e. if a ColumnIndex getter is linked to the text of the header row cell, then it gets the value or the ColumnIndex getter fails… and thus so does the IData_Get procedure… Thanks!

    No… I really didn’t want to follow Alice…

    Liked by 1 person

  6. This is a great post and truly resourceful!

    I’m an accountant and mostly write macros to get tasks done automatically. My downside is that my macros become less maintainable as I don’t always adhere to OOP error handling and running Unit tests. Do you have any VBA resource recommendations to learn about Unit tests in depth? Or is this a skill thats developed through projects?

    Like

    1. Well there’s.. this blog 😉 ..the principles underpinning OOP, SOLID, and unit testing are very much language-agnostic, so don’t confine your research to VBA! I have an article specifically about dependency injection and inversion of control coming up, hope it helps!

      Like

  7. Mathieu, please explain why you did it byval not byref, sorry for stupid question…
    Public Sub Run(ByVal dataService As IDataService, ByVal wsService As IWorksheetService)

    Like

Leave a comment