A fluent unit testing framework in VBA

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.