Unit testing in VBA is... lacking. (What isn't lacking in VBA though?) Since I've become more interested in unit testing lately, I decided I needed something better than
Debug.Assert(), so I started building this framework. Currently there is a ton of functionality missing, but since I'm new to unit testing and interfaces, I don't want to get too deep before realizing I've made a huge mistake. The code is simple, but works just fine.
I want to be able to run the output to either a file or the immediate window, so I created a simple
IOutput interface that contains one subroutine.
Public Sub PrintLine(Optional ByVal object As Variant) End Sub
Console class implementing it.
VBPredeclaredId = True to create a default instance. The Logger class remains unimplemented for the moment.
Implements IOutput Public Sub PrintLine(Optional ByVal object As Variant) If IsMissing(object) Then 'newline Debug.Print vbNullString Else Debug.Print object End If End Sub Private Sub IOutput_PrintLine(Optional ByVal object As Variant) PrintLine object End Sub
UnitTest class then takes in an
IOutput object in and stores it as a property. I need the Output stream to be available to the local project, but I don't want to expose it to external projects referencing it, so I declared it at a
Friend scope (more on that later).
Private Type TUnitTest Name As String OutStream As IOutput Assert As Assert End Type Private this As TUnitTest Public Property Get Name() As String Name = this.Name End Property Friend Property Get OutStream() As IOutput Set OutStream = this.OutStream End Property Public Property Get Assert() As Assert Set Assert = this.Assert End Property Friend Sub Initialize(Name As String, out As IOutput) this.Name = Name Set this.OutStream = out Set this.Assert = New Assert Set this.Assert.Parent = Me End Sub
UnitTest creates it's own instance of the
Assert object. I have a real concern here. I don't like that I have to pass in the test name along with the actual conditions I'm testing.
Private Const PASS As String = "Pass" Private Const FAIL As String = "Fail" Private Type TAssert Parent As UnitTest End Type Private this As TAssert Public Static Property Get Parent() As UnitTest Set Parent = this.Parent End Property Public Static Property Set Parent(ByVal Value As UnitTest) Set this.Parent = Value End Property Public Sub IsTrue(testName As String, condition As Boolean, Optional message As String) Dim output As String output = IIf(condition, PASS, FAIL) Report testName, output, message End Sub Public Sub IsFalse(testName As String, condition As Boolean, Optional message As String) Dim output As String output = IIf(condition, FAIL, PASS) Report testName, output, message End Sub Private Sub Report(testName As String, output As String, message As String) output = this.Parent.Name & "." & testName & ": " & output If message <> vbNullString Then output = output & ": " & message End If this.Parent.OutStream.PrintLine output End Sub
Finally, I don't want to import all of these classes into each project I'm working on. It will be a nightmare to keep them all synced as I make changes to the VBAUnit project. So I changed their instancing to "PublicNotCreatable".
If the instancing property is PublicNotCreatable, the class behaves normally when used within the same project, but a variable can be declared of that class type in other projects. The other project cannot create a new instance of the class, but can have a variable of the class's type. To allow another project to use a new instance of the class, the project containing the class must provide a global-scope function that creates a new instance of a class and returns it to the caller. For example, suppose Project1 contains a class named Class1, whose Instancing property is PublicNotCreatable. Suppose also that Project2 references Project1.
So I have a regular *.bas module named
Provider that contains this single function.
Public Function New_UnitTest(Name As String, out As IOutput) As UnitTest Set New_UnitTest = New UnitTest New_UnitTest.Initialize Name, out End Function
Then, from another project, I go just add VBAUnit to the references. (If you don't have it open, you have to click browse and navigate to the actual file.) I did just that and wrote some tests that essentially test themselves.
This is where the
Friend scope comes into play.
VBAUnit has access to the
Initialize subroutine and to the
OutputStream property, but they're not visible to any external projects.
This code is boilerplate code. Each new test I create will need these line. Also, once I have implemented a file logger, this is where you would need to decide where to output the results to. I don't like boiler plate, but I can't think of a way to get around it. I'm way open to suggestion on this.
Private test As VBAUnit.UnitTest Private Sub Class_Initialize() Set test = VBAUnit.New_UnitTest(TypeName(Me), VBAUnit.Console) End Sub Private Sub Class_Terminate() Set test = Nothing End Sub
Followed by the actual tests.
Public Sub RunAllTests() IsTrueShouldPass IsTrueShouldFail IsFalseShouldPass IsFalseShouldFail End Sub Public Sub IsTrueShouldPass() test.Assert.IsTrue "IsTrueShouldPass", True End Sub Public Sub IsTrueShouldFail() test.Assert.IsTrue "IsTrueShouldFail", False End Sub Public Sub IsFalseShouldPass() test.Assert.IsFalse "IsFalseShouldPass", False, "with a message." End Sub Public Sub IsFalseShouldFail() test.Assert.IsFalse "IsFalseShouldFail", True, "with a message." End Sub
Finally, in this project, we have a regular *bas. This is just kind of throw away code that we use to run the tests we're interested in.
Public Sub TestTheTests() Dim test As New AssertConditionTest test.RunAllTests test.IsFalseShouldPass End Sub
- Am I using interfaces in an intelligent way?
- Is there anyway to ditch the boilerplate code in
How can I avoid passing a "Test name" into each Assert statement and still get results like this? My method feels like a dirty hack at best.
AssertConditionTest.IsTrueShouldPass: Pass AssertConditionTest.IsTrueShouldFail: Fail AssertConditionTest.IsFalseShouldPass: Pass: with a message. AssertConditionTest.IsFalseShouldFail: Fail: with a message.
Was it a stupid decision to make
Assertit's own class and keeping a Parent UnitTest property?