3
\$\begingroup\$

A little over a year ago, I asked for feedback on Code Review for a unit testing framework I created in VBA. Development of this project has been off and on for the past year. Sometimes I wouldn't touch the project for months. Other times, I'd be working very intensely on pushing updates. The project has changed significantly since I last submitted it. So I thought I'd submit an updated version again for additional review. Below are some changes I've made since I submitted my request for Feedback.

Two Fluent objects

One thing that was suggested to me when I previously submitted the project was to extend the project by adding a method like Of(). This of method could be used to set the test value. This was an alternative approach to using the TestValue property. I implemented this change in the framework. The final design has two Fluent objects: cFluent and cFluentOf. These objects implement the TestValue property and the Of() method respectively. You can see an example of how cFluent objects work below:

Sub FluentUnitTestExample1
    Dim Result As cFluent
    Dim returnedResult As Variant
    
    '//Arrange
    Set Result = New cFluent
    returnedResult = returnVal(5)
    
    '//Act
    Result.TestValue = returnedResult
    
    '//Assert
    Debug.Assert Result.Should.Be.EqualTo(5)
End Sub

Public Function returnVal(value As Variant) As Variant
    returnVal = value
End Function

And here's a few examples of how cFluentOf objects work:

Sub FluentUnitTestExample2()
    Dim Result As cFluentOf
    Dim returnedResult As Variant
    
    '//arrange
    Set Result = New cFluentOf
    returnedResult = returnVal(5)
    
    '//Act
    With Result.Of(returnedResult)
        '//Assert
        Debug.Assert .Should.Be.EqualTo(5)
    End With
End Sub

Sub FluentUnitTestExample3()
    Dim Result As cFluentOf
    Dim returnedResult As Variant
    
    '//arrange
    Set Result = New cFluentOf
    returnedResult = returnVal(5)
    
    '//Act & Assert
    Debug.Assert Result.Of(returnedResult).Should.Be.EqualTo(5)
End Sub

Meta object updates

ApproximateEqual property: This property can be used set Approximate Equality comparisons in the EqualTo method. This is set to false by default.

Epsilon property: This property can be used to set the precision for Double value comparisons. The default value is 0.000001

TestCount property: As the name implies, this property keeps track of how many tests have been run.

A few new objects have been added that are accessible in the Meta object: The Printing object and the TestResult object.

You can see some examples below:

' //Approximate equality tests
testFluent.Meta.ApproximateEqual = True
fluent.TestValue = testFluent.Of("10").Should.Be.EqualTo(10)
Debug.Assert fluent.Should.Be.EqualTo(True)
Debug.Assert fluent.ShouldNot.Be.EqualTo(False)

testFluent.Meta.ApproximateEqual = True
fluent.TestValue = testFluent.Of("True").Should.Be.EqualTo(True)
Debug.Assert fluent.Should.Be.EqualTo(True)
Debug.Assert fluent.ShouldNot.Be.EqualTo(False)

'//default epsilon for double comparisons is 0.000001
'//the default can be modified by setting a value
'//for the epsilon property in the Meta object.

testFluent.Meta.ApproximateEqual = True
fluent.TestValue = testFluent.Of(5.0000001).Should.Be.EqualTo(5)
Debug.Assert fluent.Should.Be.EqualTo(True)
Debug.Assert fluent.ShouldNot.Be.EqualTo(False)
testFluent.Meta.ApproximateEqual = False

Printing object

The printing object supports printing the tests results in a few different ways. Under the hood, tests messages are implemented using what I call the FluentPath.

FluentPath: The FluentPath is the chain of objects that were followed to get to an object's testing method. E.g. Let's say I wanted to use the EqualTo() method to test whether a test was equal to 5. This is for a testValue of 5. The object chain would be something like Result.Should.Be.EqualTo(5). And so the corresponding Fluent Path would be "Should be equal to 5. Actual: 5"

I'll describe some of the methods for printing below:

PrintToImmediate: Prints the tests results to the immediate window

PrintToSheet: Creates an instance of Excel, outputs the test results into an Excel ListObject, and applies conditional formatting that identify whether the tests passed (Green) or tests failed (Red).

You can see examples of what the output of the PrintToSheet() method looks like below:

enter image description here

Category property: Allows you to add a category to categorize tests in a particular group.

PassedMessage property: Allows you to set a custom message for when tests pass. This is set to PASSED by default.

FailedMessage property: Allows you to set a custom message for when tests fail. This is set to FAILED by default.

You can see an example of what these properties and methods look like below:

Public Sub runMainTests()
    Dim fluent As cFluent
    Dim testFluent As cFluentOf
    
    Set fluent = New cFluent
    Set testFluent = New cFluentOf
    
    With fluent.Meta.Printing
        .TestName = "Result"
        .PassedMessage = "Success"
        .FailedMessage = "Failure"
    End With
    
    fluent.Meta.Printing.Category = "metaTests"
    testFluent.Meta.Printing.Category = "metaTests"
    Call MetaTests(fluent, testFluent)

    Debug.Print "All tests Finished!"
    Call printTestCount(testFluent.Meta.TestCount)
    
    fluent.Meta.Printing.PrintToSheet
    fluent.Meta.Printing.PrintToImmediate
End Sub

TestResult object

The TestResult object allows users to utilize UserDefinedEvents. The TestResult implements a CheckTest() method that raises three events: TestCompleted(), TestPassed(), and TestFailed(). TestCompleted is raised every time a test is run. TestPassed and TestFailed are conditionally raised depending on whether a test passed or failed respectively. Each of the events is passed a dictionary object called TestDict which contains a variety of information about the test(s). You can see an example of how these might be used below:

Private WithEvents TestResult As cTestResult

Sub DoWork()
    Dim fluent As cFluent
    
    Set fluent = New cFluent
    Set TestResult = fluent.Meta.TestResult
    
    fluent.TestValue = 10
    fluent.Should.Be.EqualTo (10)
    fluent.ShouldNot.Be.EqualTo (11)
End Sub

Private Sub TestResult_TestCompleted(TestDict As Scripting.Dictionary)
    IterateTestResult TestDict
End Sub

Private Sub testResult_TestFailed(TestDict As Scripting.Dictionary)
    IterateTestResult TestDict
End Sub

Private Sub testResult_TestPassed(TestDict As Scripting.Dictionary)
    IterateTestResult TestDict
End Sub

Private Sub IterateTestResult(d As Scripting.Dictionary)
    Dim elem As Variant
    
    For Each elem In d
        Debug.Print elem & ": " & d(elem)
    Next elem
    
    Debug.Print
End Sub

Should object updates

EvaluateTo method: The should object now implements an EvaluateTo() method. This is similar to the EqualTo() method, but there are differences. You can see some examples below:

fluent.TestValue = testFluent.Of("0").Should.EvaluateTo(False)
Debug.Assert fluent.Should.Be.EqualTo(True)
Debug.Assert fluent.ShouldNot.Be.EqualTo(False)

fluent.TestValue = testFluent.Of("0").Should.EvaluateTo(True)
Debug.Assert fluent.Should.Be.EqualTo(False)
Debug.Assert fluent.ShouldNot.Be.EqualTo(True)

fluent.TestValue = testFluent.Of(5 + 5).Should.EvaluateTo(10)
Debug.Assert fluent.Should.Be.EqualTo(True)
Debug.Assert fluent.ShouldNot.Be.EqualTo(False)

fluent.TestValue = testFluent.Of("5 + 5").Should.EvaluateTo(10)
Debug.Assert fluent.Should.Be.EqualTo(True)
Debug.Assert fluent.ShouldNot.Be.EqualTo(False)

Be object updates

The Be object was updated with a variety of methods. I'll go into detail on some of the new methods I updated below:

InDataStructure method: InDataStructure checks if a testing value is in a given data structure. The types of data structures supported are arrays, dictionaries, collections, and arraylists. For arrays, single dimensional and two dimensional or more arrays can be supported.

InDataStructure also supports nested data structures. So InDataStructure supported jagged arrays. And it can support something like a collection that contains a jagged array.

InDataStructure is capable of searching nested data structures using both recursive and iterative algorithms. This is determined by use of the flMethod enum. This enum has two values: flRecursive and flIterative. These are used to use the recursive or iterative algorithms respectively. flRecursive is the default. But the user could use flIterative if they run into a StackOverflow problem for example.

You can see some examples of the different types of tests InDataStructure supports below:

ReDim arr(1, 1, 1)
arr(0, 0, 0) = 6
arr(0, 0, 1) = 7
arr(0, 1, 0) = 8
arr(0, 1, 1) = 9
arr(1, 0, 0) = 10
arr(1, 0, 1) = 11
arr(1, 1, 0) = 12
arr(1, 1, 1) = 13
fluent.TestValue = testFluent.Of(10).ShouldNot.Be.InDataStructure(arr)
Debug.Assert fluent.Should.Be.EqualTo(False)
Debug.Assert fluent.ShouldNot.Be.EqualTo(True)

arr = Array(9, Array(10, Array(11)))
fluent.TestValue = testFluent.Of(10).ShouldNot.Be.InDataStructure(arr)
Debug.Assert fluent.Should.Be.EqualTo(False)
Debug.Assert fluent.ShouldNot.Be.EqualTo(True)

Set Col = New Collection
Col.Add 9
Col.Add 10
Col.Add 11
fluent.TestValue = testFluent.Of(10).ShouldNot.Be.InDataStructure(Col)
Debug.Assert fluent.Should.Be.EqualTo(False)
Debug.Assert fluent.ShouldNot.Be.EqualTo(True)

Set Col = New Collection
Col.Add 9
Col.Add Array(10, Array(11))
fluent.TestValue = testFluent.Of(10).ShouldNot.Be.InDataStructure(Col)
Debug.Assert fluent.Should.Be.EqualTo(False)
Debug.Assert fluent.ShouldNot.Be.EqualTo(True)
Set Col = Nothing

Something method: Checks whether an object is not equal to nothing. You can see some examples below:

Set Col = New Collection
fluent.TestValue = testFluent.Of(Col).Should.Be.Something
Debug.Assert fluent.Should.Be.EqualTo(True)
Debug.Assert fluent.ShouldNot.Be.EqualTo(False)

Set Col = Nothing
fluent.TestValue = testFluent.Of(Col).Should.Be.Something
Debug.Assert fluent.Should.Be.EqualTo(False)
Debug.Assert fluent.ShouldNot.Be.EqualTo(True)

OneOf method: Checks to see whether an object is one of a range of values. These can be both values and objects. You can see some examples below:

Set Col = New Collection
Set d = New Dictionary
fluent.TestValue = testFluent.Of(Col).Should.Be.OneOf(Col, d)
Debug.Assert fluent.Should.Be.EqualTo(True)
Debug.Assert fluent.ShouldNot.Be.EqualTo(False)
Set d = Nothing

fluent.TestValue = testFluent.Of(10).Should.Be.OneOf(Col, d, 10)
Debug.Assert fluent.Should.Be.EqualTo(True)
Debug.Assert fluent.ShouldNot.Be.EqualTo(False)

Have object updates

LengthBetween method: Checks whether an object has a length between a lower value and a higher value. You can see an example below:

fluent.TestValue = testFluent.Of(10).Should.Have.LengthBetween(1, 3)
Debug.Assert fluent.Should.Be.EqualTo(True)
Debug.Assert fluent.ShouldNot.Be.EqualTo(False)

External project updates

All of the class modules in Fluent VBA are public not creatable. So they support being used in external projects. This is done using the MakeFluent or MakingFluentOf for cFluent and cFluentOf objects respectively.

As a new update, I've written a PowerShell script that will create Office files that implement the Fluent VBA modules and required references. The current list of files created are Word, Excel, PowerPoint and Access. If you want to use Fluent VBA with Outlook, you will have import the modules from the source directory. These five programs are the only Office programs tested. So Fluent VBA may not work with other Office programs.

Misc. notes

In the initial post I made a year ago, I was given very good feedback. Many of the suggestions I implemented but some I did not. So I wanted to give some comments on why I didn't implement certain changes:

Setting VB_PredeclaredId = true: I don't think having coding behavior modified by hidden attributes that are not immediately visible is a good idea personally. That might be acceptable if the pros significantly outweighed the cons. But I don't think that's the case here. One potential future update is modifying Fluent VBA to use auto-instancing. This won't have the cons but will have a similar effect.

.AndAlso() method: This method was suggested for chaining tests together. This is similar to what something like Fluent Assertions (which inspired this project) has in C#. This isn't necessary however. VBA's native And operator can be used to accomplish this.

Let me know if you have any feedback.

Thanks!

\$\endgroup\$
4
  • 1
    \$\begingroup\$ I appreciate you want feedback on the high level api, but we still need some actual code to review (you can't just link to the entire repo). It is possible to post very large amounts of code here in which case you'll likely get reviews that are more abut your general approach. Alternatively, post the top level modules and classes which constitute your public API, and outline the interfaces (I/O) of any methods they call internally. \$\endgroup\$
    – Greedo
    Sep 26, 2022 at 13:40
  • \$\begingroup\$ Quick notes: For approximate equality see this code I posted: codereview.stackexchange.com/q/263839/146810 - it is IMO better than the epsilon based approach as often the relative difference between new numbers is key. On AndAlso; the idea is to short circuit the unit test, the VBA And operator must evaluate both the LHS (left hand side) and RHS before returning, AndAlso would never evaluate RHS if the LHS is false, making it more efficient. \$\endgroup\$
    – Greedo
    Sep 26, 2022 at 13:44
  • \$\begingroup\$ Ah that's fair. I added examples. And I was unaware that VBA's And operator does not short-circuit (but I'm not surprised tbh.) So perhaps that's something I'll consider in the future. \$\endgroup\$ Sep 26, 2022 at 14:44
  • 1
    \$\begingroup\$ I will also review the code for approximate equality. Thanks! \$\endgroup\$ Sep 26, 2022 at 22:15

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.