I wanted to get some feedback on a fluent unit testing framework I’ve written. I call the project Fluent VBA. You can find a link to the project on GitHub here
Motivation
This project was inspired when I read about Fluent Assertions in C# as I was reading a book on unit testing.
Usage
Fluent frameworks are intended to be read like natural language. So instead of having something like:
Dim result = returnsFive() ‘returns the number 5
Dim Assert as cUnitTester
Set Assert = New cUnitTester
Assert.Equal(Result,5)
You can have code that reads more naturally like so:
Dim Result as cFluent
Set Result = new cFluent
Result.TestValue = ReturnsFive()
Result.Should.Be.EqualTo(5)
High level overview
Fluent VBA is broken down into 13 class modules: Nine classes and four interfaces. All of the class modules have an instancing property of PublicNotCreatable. So the project can be referenced in an external testing project. To do that, you’d just need to create an instance of cFluent using the MakeFluent() method in the mInit module. But you don’t have to do that if you don’t want to. You can also write your testing code in the cFluent project.
The project has a few main components: A Should component, a Be component, and a Have component. I also have components for their opposite: A ShouldNot component, and NotBe component, and a NotHave component. These various components are implemented in the project using composition.
Since the project is a unit-testing framework, I can use the project to test itself. So in the mTests module, I have a procedure called MetaTests where I do this. The meta tests mainly use the Fluent.Should.Be.EqualTo method with debug.assert to do this. Since all other methods rely on this method, I test this test extensively. I also test its opposite (i.e. Fluent.ShouldNot.Be.EqualTo) to ensure that it contains the expected value. In addition to these MetaTests, I also have lots of different examples showing how you can use this framework in a variety of different ways.
Detailed overview
Interfaces:
The IShould Interface:
This interface contains the following procedures:
Public Property Get Be() As IBe
End Property
Public Property Get Have() As IHave
End Property
Public Function Contain(value As Variant) As Boolean
End Function
Public Function StartWith(value As Variant) As Boolean
End Function
Public Function EndWith(value As Variant) As Boolean
End Function
It is implemented by both the cShould and cShouldNot classes.
The IBe interface:
This interface contains the following procedures:
Public Function GreaterThan(value As Variant) As Boolean
End Function
Public Function LessThan(value As Variant) As Boolean
End Function
Public Function EqualTo(value As Variant) As Boolean
End Function
It is implemented by both the cBe and cNotBe classes.
The IHave interface
This interface contains the following procedures:
Public Function LengthOf(value As Double) As Boolean
End Function
Public Function MaxLengthOf(value As Double) As Boolean
End Function
Public Function MinLengthOf(value As Double) As Boolean
End Function
It is implemented by both the cHave and cNotHave classes.
The ISetExpression interface:
This interface implements the following procedure:
Public Property Set setExpr(value As cExpressions)
End Property
It is implemented by the cBe, cNotBe, cHave, cNotHave, cShould, and cShouldNot classes.
Classes
The cFluent class
The highest level object in the project. It is responsible for accepting the initial test value. From the client, you can access the cMeta class to access meta-level test properties. And you can use the cShould and cShouldNot classes to access additional classes to be described.
This is the code in the cFluent class:
Option Explicit
Private pShould As cShould
Private pShouldSet As ISetExpression
Private pShouldNot As cShouldNot
Private pShouldNotSet As ISetExpression
Private pExpressions As cExpressions
Private pMeta As cMeta
Private pMetaSet As ISetExpression
Public Property Let TestValue(value As Variant)
pExpressions.TestValue = value
End Property
Public Property Get TestValue() As Variant
TestValue = pExpressions.TestValue
End Property
Public Property Get Should() As IShould
If pShould Is Nothing Then
Set pShould = New cShould
End If
Set pShouldSet = pShould
Set pShouldSet.setExpr = pExpressions
Set Should = pShouldSet
End Property
Public Property Get ShouldNot() As IShould
If pShouldNot Is Nothing Then
Set pShouldNot = New cShouldNot
End If
Set pShouldNotSet = pShouldNot
Set pShouldNotSet.setExpr = pExpressions
Set ShouldNot = pShouldNotSet
End Property
Public Property Get Meta() As cMeta
Set Meta = pMeta
End Property
Private Sub Class_Initialize()
Set pExpressions = New cExpressions
Set pMeta = New cMeta
Set pExpressions.setMeta = pMeta
End Sub
The cMeta class
This object is responsible for some test-related settings. These are both implemented as properties which both implement setters and getters. The PrintResult property is a Boolean property. If the property is set to true, results of the results are printed in the immediate window. The second is the TestName field. If it’s given a value, that value is printed to the immediate window when the PrintResults property is set to true.
This is the code in the cMeta class:
Option Explicit
Private pPrintResults As Boolean
Private pTestName As String
Public Property Let TestName(value As String)
pTestName = value
End Property
Public Property Get TestName() As String
TestName = pTestName
End Property
Public Property Let PrintResults(value As Boolean)
pPrintResults = value
End Property
Public Property Get PrintResults() As Boolean
PrintResults = pPrintResults
End Property
The cExpressions class
This object is responsible for the evaluation and printing of all expressions. It contains all methods for evaluation. It also uses an instance of cMeta to determine if and how tests are to be printed. And it contains the TestValue value which the tests are to be evaluated against.
This is the code in the cExpressions class:
Option Explicit
Private pTestValue As Variant
Private pMeta As cMeta
Public Property Let TestValue(value As Variant)
pTestValue = value
End Property
Public Property Get TestValue() As Variant
TestValue = pTestValue
End Property
Public Property Set setMeta(value As cMeta)
Set pMeta = value
End Property
Public Function GreaterThan(OrigVal As Variant, NewVal As Variant, Optional NegateValue As Boolean = False) As Boolean
GreaterThan = (OrigVal > NewVal)
If pMeta.PrintResults Then
If NegateValue Then
NegateValue = Not GreaterThan
PrintEval (NegateValue)
Else
PrintEval (GreaterThan)
End If
End If
End Function
Public Function LessThan(OrigVal As Variant, NewVal As Variant, Optional NegateValue As Boolean = False) As Boolean
LessThan = (OrigVal < NewVal)
If pMeta.PrintResults Then
If NegateValue Then
NegateValue = Not LessThan
PrintEval (NegateValue)
Else
PrintEval (LessThan)
End If
End If
End Function
Public Function EqualTo(OrigVal As Variant, NewVal As Variant, Optional NegateValue As Boolean = False) As Boolean
EqualTo = (OrigVal = NewVal)
If pMeta.PrintResults Then
If NegateValue Then
NegateValue = Not EqualTo
PrintEval (NegateValue)
Else
PrintEval (EqualTo)
End If
End If
End Function
Public Function Contain(OrigVal As Variant, NewVal As Variant, Optional NegateValue As Boolean = False) As Boolean
If OrigVal Like "*" & NewVal & "*" Then
Contain = True
End If
If pMeta.PrintResults Then
If NegateValue Then
NegateValue = Not Contain
PrintEval (NegateValue)
Else
PrintEval (Contain)
End If
End If
End Function
Public Function StartWith(OrigVal As Variant, NewVal As Variant, Optional NegateValue As Boolean = False) As Boolean
Dim valLength As Long
valLength = Len(NewVal)
If Left(OrigVal, valLength) = CStr(NewVal) Then
StartWith = True
End If
If pMeta.PrintResults Then
If NegateValue Then
NegateValue = Not StartWith
PrintEval (NegateValue)
Else
PrintEval (StartWith)
End If
End If
End Function
Public Function EndWith(OrigVal As Variant, NewVal As Variant, Optional NegateValue As Boolean = False) As Boolean
Dim valLength As Long
valLength = Len(NewVal)
If Right(OrigVal, valLength) = CStr(NewVal) Then
EndWith = True
End If
If pMeta.PrintResults Then
If NegateValue Then
NegateValue = Not EndWith
PrintEval (NegateValue)
Else
PrintEval (EndWith)
End If
End If
End Function
Public Function LengthOf(OrigVal As Double, NewVal As Double, Optional NegateValue As Boolean = False) As Boolean
LengthOf = (Len(CStr(OrigVal)) = NewVal)
If pMeta.PrintResults Then
If NegateValue Then
NegateValue = Not LengthOf
PrintEval (NegateValue)
Else
PrintEval (LengthOf)
End If
End If
End Function
Public Function MaxLengthOf(OrigVal As Double, NewVal As Double, Optional NegateValue As Boolean = False) As Boolean
MaxLengthOf = (Len(CStr(OrigVal)) <= NewVal)
If pMeta.PrintResults Then
If NegateValue Then
NegateValue = Not MaxLengthOf
PrintEval (NegateValue)
Else
PrintEval (MaxLengthOf)
End If
End If
End Function
Public Function MinLengthOf(OrigVal As Double, NewVal As Double, Optional NegateValue As Boolean = False) As Boolean
MinLengthOf = (Len(CStr(OrigVal)) >= NewVal)
If pMeta.PrintResults Then
If NegateValue Then
NegateValue = Not MinLengthOf
PrintEval (NegateValue)
Else
PrintEval (MinLengthOf)
End If
End If
End Function
Friend Sub PrintEval(ByVal value As Boolean)
Dim Result As String
Dim TestPassed As Boolean
Result = ""
TestPassed = value
If TestPassed Then
Result = "Passed"
If pMeta.TestName <> Empty Then
Debug.Print pMeta.TestName & Result
Else
Debug.Print "Passed: " & Result
End If
Else
Result = "Failed"
If pMeta.TestName <> Empty Then
Debug.Print pMeta.TestName & Result
Else
Debug.Print "Failed: " & Result
End If
End If
End Sub
The cShould class
Responsible for creating instances of the Have and Be classes. Also responsible for testing a few methods described in the IShould interface. These methods use methods implemented by the cExpressions object under the hood.
This is the code in the cShould class:
Option Explicit
Implements IShould
Implements ISetExpression
Private pShouldVal As Variant
Private pBe As cBe
Private pBeSet As ISetExpression
Private pHave As cHave
Private pHaveSet As ISetExpression
Private pExpressions As cExpressions
Public Property Set ISetExpression_setExpr(value As cExpressions)
Set pExpressions = value
pShouldVal = pExpressions.TestValue
End Property
Public Property Get IShould_Have() As IHave
If pHave Is Nothing Then
Set pHave = New cHave
End If
Set pHaveSet = pHave
Set pHaveSet.setExpr = pExpressions
Set IShould_Have = pHaveSet
End Property
Public Property Get IShould_Be() As IBe
If pBe Is Nothing Then
Set pBe = New cBe
End If
Set pBeSet = pBe
Set pBeSet.setExpr = pExpressions
Set IShould_Be = pBeSet
End Property
Public Function IShould_Contain(value As Variant) As Boolean
IShould_Contain = pExpressions.Contain(pShouldVal, value)
End Function
Public Function IShould_StartWith(value As Variant) As Boolean
IShould_StartWith = pExpressions.StartWith(pShouldVal, value)
End Function
Public Function IShould_EndWith(value As Variant) As Boolean
IShould_EndWith = pExpressions.EndWith(pShouldVal, value)
End Function
The cBe class
Responsible for implementing and executing the methods described earlier in the IBe interface. These methods use methods implemented by the cExpressions object under the hood.
This is the code in the cBe class:
Option Explicit
Implements IBe
Implements ISetExpression
Private pExpressions As cExpressions
Private pBeValue As Variant
Public Property Set ISetExpression_setExpr(value As cExpressions)
Set pExpressions = value
pBeValue = pExpressions.TestValue
End Property
Public Function IBe_GreaterThan(value As Variant) As Boolean
IBe_GreaterThan = pExpressions.GreaterThan(pBeValue, value)
End Function
Public Function IBe_LessThan(value As Variant) As Boolean
IBe_LessThan = pExpressions.LessThan(pBeValue, value)
End Function
Public Function IBe_EqualTo(value As Variant) As Boolean
IBe_EqualTo = pExpressions.EqualTo(pBeValue, value)
End Function
The cHave class
Responsible for implementing and executing the methods described earlier in the IHave interface. These methods use methods implemented by the cExpressions object under the hood.
This is the code in the cHave class:
Option Explicit
Implements IHave
Implements ISetExpression
Private pExpressions As cExpressions
Private pHaveVal As Variant
Public Property Set ISetExpression_setExpr(value As cExpressions)
Set pExpressions = value
pHaveVal = pExpressions.TestValue
End Property
Public Function IHave_LengthOf(value As Double) As Boolean
IHave_LengthOf = pExpressions.LengthOf(CDbl(pHaveVal), value)
End Function
Public Function IHave_MaxLengthOf(value As Double) As Boolean
IHave_MaxLengthOf = pExpressions.MaxLengthOf(CDbl(pHaveVal), value)
End Function
Public Function IHave_MinLengthOf(value As Double) As Boolean
IHave_MinLengthOf = pExpressions.MinLengthOf(CDbl(pHaveVal), value)
End Function
The Not classes (cShouldNot,cNotBe, cNotHave) Responsible for implementing and executing the methods in their respective interfaces (i.e. IShould, IBe, and IHave) For the implementation of the various methods, they use the same methods in the cExpessions object as their non-negated counterparts. The only difference is that these methods are negated with a not operator to get the opposite result.
The cShouldNot class
This is the code in the cShouldNot class:
Option Explicit
Implements IShould
Implements ISetExpression
Private pNotBe As cNotBe
Private pNotBeSet As ISetExpression
Private pNotHave As cNotHave
Private pNotHaveSet As ISetExpression
Private pExpressions As cExpressions
Private pShouldNotVal As Variant
Public Property Set ISetExpression_setExpr(value As cExpressions)
Set pExpressions = value
pShouldNotVal = pExpressions.TestValue
End Property
Public Property Get IShould_Have() As IHave
If pNotHave Is Nothing Then
Set pNotHave = New cNotHave
End If
Set pNotHaveSet = pNotHave
Set pNotHaveSet.setExpr = pExpressions
Set IShould_Have = pNotHaveSet
End Property
Public Property Get IShould_Be() As IBe
If pNotBe Is Nothing Then
Set pNotBe = New cNotBe
End If
Set pNotBeSet = pNotBe
Set pNotBeSet.setExpr = pExpressions
Set IShould_Be = pNotBeSet
End Property
Public Function IShould_Contain(value As Variant) As Boolean
IShould_Contain = Not pExpressions.Contain(pShouldNotVal, value, True)
End Function
Public Function IShould_StartWith(value As Variant) As Boolean
IShould_StartWith = Not pExpressions.StartWith(pShouldNotVal, value, True)
End Function
Public Function IShould_EndWith(value As Variant) As Boolean
IShould_EndWith = Not pExpressions.EndWith(pShouldNotVal, value, True)
End Function
The cNotBe class
This is the code in the cNotBe class:
Option Explicit
Implements IBe
Implements ISetExpression
Private pNotBeValue As Variant
Private pBe As IBe
Private pExpressions As cExpressions
Public Property Set ISetExpression_setExpr(value As cExpressions)
Set pExpressions = value
pNotBeValue = pExpressions.TestValue
End Property
Public Function IBe_GreaterThan(value As Variant) As Boolean
IBe_GreaterThan = Not pExpressions.GreaterThan(pNotBeValue, value, True)
End Function
Public Function IBe_LessThan(value As Variant) As Boolean
IBe_LessThan = Not pExpressions.LessThan(pNotBeValue, value, True)
End Function
Public Function IBe_EqualTo(value As Variant) As Boolean
IBe_EqualTo = Not pExpressions.EqualTo(pNotBeValue, value, True)
End Function
The cNotHave class
This is the code in the cNotHave class:
Option Explicit
Implements IHave
Implements ISetExpression
Private pNotHaveVal As Variant
Private pExpressions As cExpressions
Public Property Set ISetExpression_setExpr(value As cExpressions)
Set pExpressions = value
pNotHaveVal = pExpressions.TestValue
End Property
Public Function IHave_LengthOf(value As Double) As Boolean
IHave_LengthOf = Not pExpressions.LengthOf(CDbl(pNotHaveVal), value, True)
End Function
Public Function IHave_MaxLengthOf(value As Double) As Boolean
IHave_MaxLengthOf = Not pExpressions.MaxLengthOf(CDbl(pNotHaveVal), value, True)
End Function
Public Function IHave_MinLengthOf(value As Double) As Boolean
IHave_MinLengthOf = Not pExpressions.MinLengthOf(CDbl(pNotHaveVal), value, True)
End Function
Final notes
After LOTS of changes to the API, I think I finally have a design I’m satisfied with. I’d appreciate any feedback.