patternMinor
Unit Testing - A Better Solution
Viewed 0 times
bettersolutiontestingunit
Problem
Following-up on the Automagic testing framework for VBA review, I've refactored much of the
Refactoring part of the
I never thought I'd say this, but it's a good thing VBA doesn't support multithreaded execution. I'd have gone mad if it were the case.
Here's the output (in the immediate pane) for a
This time I've thoroughly verified the output, it's exactly what's expected - in case I missed anything, here's the
```
Option Explicit
Public Sub ThisIsNoTest()
Err.Raise 5
End Sub
'@TestMethod
Public Sub MagicCommentWorks()
End Sub
Private Sub TestPrivateMethod()
TestEngine static class, and introduced a TestMethod class.Refactoring part of the
TestEngine into its own class introduced new problems that needed new solutions; because the client code is using the Assert class default instance, each TestMethod instance had to be listening for the same instance's events - VBA events cannot be registered/unregistered at will, so I introduced a ListenAssertEvents flag in each instance, that would only be True when that instance is running.I never thought I'd say this, but it's a good thing VBA doesn't support multithreaded execution. I'd have gone mad if it were the case.
Here's the output (in the immediate pane) for a
TestClass1 class in a client VBA project named VBAProject:TestEngine.RunAllTests "VBAProject", new TestClass1
2014-09-21 20:19:44 MagicCommentWorks: [INCONCLUSIVE] - No assertions made.
2014-09-21 20:19:44 TestAreEqual: [PASS]
2014-09-21 20:19:44 TestAreNotEqual: [PASS]
2014-09-21 20:19:44 TestAreSame: [FAIL] - AreSame failed: Objects should be same reference.
2014-09-21 20:19:44 TestAreNotSame: [PASS]
2014-09-21 20:19:44 TestFail: [FAIL] - Fail failed: This wasn't meant to be.
2014-09-21 20:19:44 TestInconclusive: [INCONCLUSIVE] - No idea.
2014-09-21 20:19:44 TestIsFalse: [PASS]
2014-09-21 20:19:44 TestIsNothing: [PASS]
2014-09-21 20:19:44 TestIsNotNothing: [PASS]
2014-09-21 20:19:44 TestIsTrue: [PASS]
2014-09-21 20:19:44 TestBlowUp: [FAIL] - AreEqual failed: This failed assertion prevents reporting the division by zero that follows.
2014-09-21 20:19:44 TestNoAssert: [INCONCLUSIVE] - No assertions made.
This time I've thoroughly verified the output, it's exactly what's expected - in case I missed anything, here's the
TestClass1 client code:```
Option Explicit
Public Sub ThisIsNoTest()
Err.Raise 5
End Sub
'@TestMethod
Public Sub MagicCommentWorks()
End Sub
Private Sub TestPrivateMethod()
Solution
TestEngine
-
I don't see these being used anywhere.
-
These are scoped to the class when they could be scoped to the
-
I'm in the air about passing the project name to
-
I'm also no sure about passing the Test class in as an
-
Here you have a bad comment followed by a good one.
Why does
-
A readability nitpick here. I would rewrite this:
Like this:
-
Last, but not least. Be careful with circular references. Each
The problem is the way COM handles garbage collection. Objects don't get destroyed until the reference count for them reaches zero. Since In this case, all of the methods will remain in memory until
I'm having a hard time explaining this, so here is Microsoft's VB6 documentation on circular references. I find the most enlightening part of this document is this diagram.
TestMethod
-
The
-
I see what you're talking about with the
-
I don't see these being used anywhere.
CurrentTest As TestMethod
CurrentTestAllResults As List
CurrentTestFailedOrInconclusiveResults As List-
These are scoped to the class when they could be scoped to the
IsTestMethod function.Private Const TestMethodNamePrefix As String = "Test"
Private Const TestMethodAttribute As String = "TestMethod"-
I'm in the air about passing the project name to
RunAllTests. You could just as easily call Application.VBE.ActiveVBProject. I don' think it's unreasonable to think that the end user is in the test project that they want to run. Of course, your way is more explicit and you may have other reasons for doing what you've done. So, take it for what it's worth (not much).-
I'm also no sure about passing the Test class in as an
Object. I suppose it could implement an empty interface, just for the typing, but I don't know if that would really be any better. Just some food for thought.-
Here you have a bad comment followed by a good one.
Dim test As TestMethod
Dim result As TestResult
Dim item As Variant
For Each item In this.RegisteredTests.Items 'requires Variant
Set test = item 'cast Variant to TestMethod
Set result = test.Run
this.Output.WriteResult test.MethodName, result
NextWhy does
this.RegisteredTest.Items require a variant?CanAddTestMethodfeels weird, but it's fine. The alternatives are worse. If there was another way to return early from the function I would recommend it, but there isn't. It's actually a lot slicker than it looks.
-
A readability nitpick here. I would rewrite this:
Dim result As Boolean
result = _
Framework.Strings.StartsWith(TestMethodNamePrefix, TestMethod.name, False) Or _
TestMethod.HasAttribute(TestMethodAttribute)Like this:
Dim result As Boolean
result = _
Framework.Strings.StartsWith(TestMethodNamePrefix, TestMethod.name, False) _
Or TestMethod.HasAttribute(TestMethodAttribute)-
Last, but not least. Be careful with circular references. Each
TestMethod here has a reference to the Test1Class, but it has no way to dispose of the TestMethods. It doesn't even know about them (which shows a good separation of concerns actually). If CanAddTestMethod(prospect, result) Then
Set tMethod = New TestMethod
Set tMethod.OwnerInstance = classInstance
tMethod.MethodName = prospect.name
result.Add tMethod.MethodName, tMethod
End IfThe problem is the way COM handles garbage collection. Objects don't get destroyed until the reference count for them reaches zero. Since In this case, all of the methods will remain in memory until
TestEngine exits it's scope. It might not cause an issue in this instance, but it would certainly be more memory efficient to get rid of them as you're finished with them. Set a breakpoint in TestMethod's Class_Terminate event and pay close attention to when (if ever) it's actually being raised. I'm having a hard time explaining this, so here is Microsoft's VB6 documentation on circular references. I find the most enlightening part of this document is this diagram.
TestMethod
-
The
Run sub..... wow man. That's error handling done right. It's a very clean routine. I like it. I know you don't like the flag, and truthfully, neither do I, but I don't see how it could be done differently.-
I see what you're talking about with the
EvaluateResults function. Unfortunately, there's not much you can do. I don't suppose your List class has a Contains function? I'm sure it would be less efficient, but it would at least abstract the complexity of contained in the Else. The only other thing I can think to do is restrict each TestMethod to one and only one assertion and I wouldn't really recommend that.Code Snippets
CurrentTest As TestMethod
CurrentTestAllResults As List
CurrentTestFailedOrInconclusiveResults As ListPrivate Const TestMethodNamePrefix As String = "Test"
Private Const TestMethodAttribute As String = "TestMethod"Dim test As TestMethod
Dim result As TestResult
Dim item As Variant
For Each item In this.RegisteredTests.Items 'requires Variant
Set test = item 'cast Variant to TestMethod
Set result = test.Run
this.Output.WriteResult test.MethodName, result
NextDim result As Boolean
result = _
Framework.Strings.StartsWith(TestMethodNamePrefix, TestMethod.name, False) Or _
TestMethod.HasAttribute(TestMethodAttribute)Dim result As Boolean
result = _
Framework.Strings.StartsWith(TestMethodNamePrefix, TestMethod.name, False) _
Or TestMethod.HasAttribute(TestMethodAttribute)Context
StackExchange Code Review Q#63506, answer score: 5
Revisions (0)
No revisions yet.