patternMajor
Unit Testing in VBA
Viewed 0 times
testingunitvba
Problem
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
I want to be able to run the output to either a file or the immediate window, so I created a simple
IOutput.cls
And a
Console.cls
The
UnitTest.cls
```
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
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. IOutput.cls
Public Sub PrintLine(Optional ByVal object As Variant)
End SubAnd a
Console class implementing it. Console uses VBPredeclaredId = True to create a default instance. The Logger class remains unimplemented for the moment. Console.cls
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 SubThe
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).UnitTest.cls
```
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
Solution
IOutput class module (Interface)
Looking at how the interface is being used:
Where
Why is the parameter optional? and why is it a Variant? ...and why is it called object? I would have expected this:
Which leads me to the implementation:
Console class module
If the parameter is a
It seems your
In the context of the
...then I'd renamed
and
That gives
UnitTest class module
Mucho kudos, this is the first time I'm seeing a warranted use of the
Impressive. I wish I had realized VBAProjects could reference each other, 10 years ago!
Provider code module
I don't like this. I would have made it a "static" class module (with a default instance), and called it
I don't like the method name either - again, underscores in identifiers are confusing in VBA. If that code is in a class called
I don't like that you're assigning the result, and then calling a method on that reference - it looks very awkward and would be much clearer with a
Assert class module
I've never seen the
Question is, how to do that?
I think I'd expose an event:
```
Public
Looking at how the interface is being used:
this.Parent.OutStream.PrintLine outputWhere
output is clearly a String, which makes sense. But the interface's signature doesn't reflect that, and is confusing:Public Sub PrintLine(Optional ByVal object As Variant)Why is the parameter optional? and why is it a Variant? ...and why is it called object? I would have expected this:
Public Sub PrintLine(ByVal output As String)Which leads me to the implementation:
Console class module
If the parameter is a
String, and isn't optional, the PrintLine implementation gets... a little bit simpler:Option Explicit 'always. even if you're not **yet** declaring anything.
Implements IOutput
Public Sub PrintLine(ByVal output As String)
Debug.Print output
End Sub
Private Sub IOutput_PrintLine(ByVal output As String)
PrintLine output
End SubIt seems your
Console class was intended to be used a bit like a .net System.Console, a static class.In the context of the
IOutput interface implementation, it doesn't make sense for that class to be static, nor to have an optional parameter to its PrintLine method. However, if you encapsulate a test result in its own class...Option Explicit
Public Enum TestOutcome
Inconclusive
Failed
Succeeded
End Enum
Private Type TTestResult
outcome As TestOutcome
output As String
End Type
Private this As TTestResult
Public Property Get TestOutcome() As TestOutcome
TestOutcome = this.outcome
End Property
Friend Property Let TestOutcome(ByVal value As TestOutcome)
this.outcome = value
End Property
Public Property Get TestOutput() As String
TestOutput = this.output
End Property
Friend Property Let TestOutput(ByVal value As String)
this.output = value
End Property
Public Function Create(ByVal outcome As TestOutcome, ByVal output As String)
Dim result As New TestResult
result.TestOutcome = outcome
result.TestOutput = output
Set Create = result
End Function...then I'd renamed
IOutput to ITestOutput, and change the signature like this:Public Sub WriteResult(ByVal result As TestResult)
End Suband
Console would look ilke this:Option Explicit
Implements ITestOutput
Public Sub WriteResult(ByVal result As TestResult)
Debug.Print result.TestOutput
End Sub
Private Sub ITestOutput_WriteResult(ByVal result As TestResult)
WriteResult result
End SubThat gives
WriteResult a much clearer intent than PrintLine, and doesn't stop you from implementing a WriteLine(String) method and keeping Console as a static utility class, and as a bonus you have a concept of a test result that can be inconclusive, failed or successful.UnitTest class module
Mucho kudos, this is the first time I'm seeing a warranted use of the
Friend keyword in VBA. This is pretty clever, and enables several things that aren't otherwise possible in VBA:- Factories: a class can now be created and initialized with parameter values, as if created with a constructor.
- Immutability: a class can only expose getters, and be immutable from the client code's perspective.
Impressive. I wish I had realized VBAProjects could reference each other, 10 years ago!
Provider code module
I don't like this. I would have made it a "static" class module (with a default instance), and called it
UnitTestFactory.I don't like the method name either - again, underscores in identifiers are confusing in VBA. If that code is in a class called
UnitTestFactory, the method's name could simply be Create.I don't like that you're assigning the result, and then calling a method on that reference - it looks very awkward and would be much clearer with a
result variable, and I would make the IOutput implementation/reference a property of the factory class, removing it from the method's signature:Option Explicit
Private Type TUnitTestFactory
TestOutput As IOutput
End Type
Private this As TUnitTestFactory
Public Property Get TestOutput() As IOutput
Set TestOutput = this.TestOutput
End Property
Public Property Set TestOutput(ByVal value As IOutput)
Set this.TestOutput = value
End Property
Public Function Create(ByVal testName As String) As UnitTest
Dim result As New UnitTest
Set result.Initialize testName, TestOutput
Set Create = result
End FunctionAssert class module
I've never seen the
Static keyword used like this (what's a static property in VBA anyway?), and the Parent property makes me think that class is doing more than it should. I believe the TestOutcome enum and the TestResult class I've suggested above, would be helpful here... but I don't think it's Assert's job to report the test's outcome - by keeping that responsibility at the test level, you remove the need to pass the test's name to the Assert methods.Question is, how to do that?
I think I'd expose an event:
```
Public
Code Snippets
this.Parent.OutStream.PrintLine outputPublic Sub PrintLine(Optional ByVal object As Variant)Public Sub PrintLine(ByVal output As String)Option Explicit 'always. even if you're not **yet** declaring anything.
Implements IOutput
Public Sub PrintLine(ByVal output As String)
Debug.Print output
End Sub
Private Sub IOutput_PrintLine(ByVal output As String)
PrintLine output
End SubOption Explicit
Public Enum TestOutcome
Inconclusive
Failed
Succeeded
End Enum
Private Type TTestResult
outcome As TestOutcome
output As String
End Type
Private this As TTestResult
Public Property Get TestOutcome() As TestOutcome
TestOutcome = this.outcome
End Property
Friend Property Let TestOutcome(ByVal value As TestOutcome)
this.outcome = value
End Property
Public Property Get TestOutput() As String
TestOutput = this.output
End Property
Friend Property Let TestOutput(ByVal value As String)
this.output = value
End Property
Public Function Create(ByVal outcome As TestOutcome, ByVal output As String)
Dim result As New TestResult
result.TestOutcome = outcome
result.TestOutput = output
Set Create = result
End FunctionContext
StackExchange Code Review Q#62781, answer score: 23
Revisions (0)
No revisions yet.