此代码需要对Visual Basic Project的可信编程访问。
1。客户端代码
我希望我的测试类看起来像这样:
TestClass1类模块(客户端代码)
Option Explicit
Public Sub ThisIsNoTest()
Err.Raise 5
End Sub
'@TestMethod
Public Sub MagicCommentWorks()
End Sub
Public Sub TestAreEqual()
assert.AreEqual 12, 12, "Values should be equal."
End Sub
Public Sub TestAreNotEqual()
assert.AreNotEqual 12, 34, "Values should not be equal."
End Sub
Public Sub TestAreSame()
assert.AreSame New Collection, New Collection, "Objects should be same reference."
End Sub
Public Sub TestAreNotSame()
assert.AreNotSame New Collection, New Collection, "Objects should not be the same reference."
End Sub
Public Sub TestFail()
assert.Fail "This wasn't meant to be."
End Sub
Public Sub TestInconclusive()
assert.Inconclusive "No idea."
End Sub
Public Sub TestIsFalse()
assert.IsFalse False, "True should be False."
End Sub
Public Sub TestIsNothing()
Dim foo As Object
assert.IsNothing foo, "Foo should be nothing."
End Sub
Public Sub TestIsNotNothing()
Dim foo As New Collection
assert.IsNotNothing foo, "Foo shouldn't be nothing."
End Sub
Public Sub TestIsTrue()
assert.IsTrue True, "False should be True."
End Sub
Public Sub TestBlowUp()
assert.IsTrue True
assert.AreEqual False, True
Debug.Print 1 / 0
assert.Fail "Test should have failed by now."
End Sub
Public Sub TestNoAssert()
End Sub
输出
我希望能够通过直接窗格中的简单“命令行”调用运行测试:
TestEngine.RunAllTests "VBAProject", New TestClass1
Registered test: TestClass1.MagicCommentWorks
Registered test: TestClass1.TestAreEqual
Registered test: TestClass1.TestAreNotEqual
Registered test: TestClass1.TestAreSame
Registered test: TestClass1.TestAreNotSame
Registered test: TestClass1.TestFail
Registered test: TestClass1.TestInconclusive
Registered test: TestClass1.TestIsFalse
Registered test: TestClass1.TestIsNothing
Registered test: TestClass1.TestIsNotNothing
Registered test: TestClass1.TestIsTrue
Registered test: TestClass1.TestBlowUp
Registered test: TestClass1.TestNoAssert
2014-09-16 00:24:20 MagicCommentWorks: [INCONCLUSIVE] - No assertions made.
2014-09-16 00:24:20 TestAreEqual: [PASS]
2014-09-16 00:24:20 TestAreNotEqual: [PASS]
2014-09-16 00:24:20 TestAreSame: [FAIL] - AreSame failed: Objects should be same reference.
2014-09-16 00:24:20 TestAreNotSame: [PASS]
2014-09-16 00:24:20 TestFail: [FAIL] - Fail failed: This wasn't meant to be.
2014-09-16 00:24:20 TestInconclusive: [PASS]
2014-09-16 00:24:20 TestIsFalse: [PASS]
2014-09-16 00:24:20 TestIsNothing: [PASS]
2014-09-16 00:24:20 TestIsNotNothing: [PASS]
2014-09-16 00:24:20 TestIsTrue: [PASS]
2014-09-16 00:24:20 TestBlowUp: [INCONCLUSIVE] - Test raised an error: Division by zero
2014-09-16 00:24:20 TestNoAssert: [INCONCLUSIVE] - No assertions made.
客户端代码必须引用Excel加载项:
UnitTesting包含测试引擎。
就这些。
UnitTesting
引用了这些.xlam Excel加载项:系统包含
Framework
“命名空间”,其中公开了诸如List
和Tuple
之类的自定义类型,以及Strings
中的各种字符串帮助器方法。名称空间”。反射由UnitTesting加载项使用,并引用Microsoft Visual Basic for Applications Extensibility 5.3库以及系统加载项。
我主要是对
UnitTesting
项目的反馈感兴趣,但Reflection
项目也向批评家开放!2。 UnitTesting
假定已安装外接程序的依赖项,此模块是客户端代码必须引用的唯一外接程序(尽管也不能禁止也引用系统)。
客户端代码唯一需要了解的是
TestEngine
的默认实例。TestEngine类模块
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "TestEngine"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = True
Option Explicit
Private Type TTestEngine
Output As ITestOutput
RegisteredTests As Dictionary
CurrentTest As String
CurrentTestResults As List
End Type
Private WithEvents assertion As Assert
Attribute assertion.VB_VarHelpID = -1
Private this As TTestEngine
Public Sub RunAllTests(ByVal projectName As String, ByRef classInstance As Object)
Set this.RegisteredTests = ReflectTestMethods(projectName, classInstance)
Dim test As Variant
For Each test In this.RegisteredTests
RunTest test
Next
End Sub
Private Function ReflectTestMethods(ByVal projectName As String, ByRef classInstance As Object) As Dictionary
Dim classMethods As List
Set classMethods = ClassModule.GetMethods(projectName, TypeName(classInstance))
Dim result As New Dictionary
Dim prospect As Method
For Each prospect In classMethods
If CanAddTestMethod(prospect, result) Then
result.Add prospect.name, Tuple.Create(classInstance, prospect.name)
Debug.Print "Registered test: " & TypeName(classInstance) & "." & prospect.name
End If
Next
Set ReflectTestMethods = result
End Function
Private Function CanAddTestMethod(ByVal prospect As Method, ByRef testMethods As Dictionary) As Boolean
Dim result As Boolean
If Not IsTestMethodName(prospect) Then Exit Function
If testMethods.Exists(prospect.name) Then Exit Function
CanAddTestMethod = True
End Function
Private Function IsTestMethodName(ByVal testMethod As Method) As Boolean
IsTestMethodName = _
Framework.Strings.StartsWith("Test", testMethod.name, False) Or _
Framework.Strings.StartsWith("'@TestMethod", Split(testMethod.Body, vbNewLine)(1), False)
End Function
Private Sub RunTest(ByVal name As String)
this.CurrentTest = name
Set this.CurrentTestResults = List.Create
Dim result As TestResult
On Error GoTo CleanFail
Dim testOutput As New DebugTestOutput
Set this.Output = testOutput
Dim test As Tuple
Set test = this.RegisteredTests(name)
CallByName test.Item1, test.Item2, VbMethod
If this.CurrentTestResults.Count = 0 Then
Set result = TestResult.Create(Inconclusive, "No assertions made.")
ElseIf CurrentTestFailedResults.Count = 0 Then
Set result = TestResult.Create(Succeeded)
ElseIf CurrentTestFailedResults.Count > 0 Then
Set result = CurrentTestFailedResults.First
End If
CleanExit:
this.Output.WriteResult this.CurrentTest, result
this.CurrentTest = vbNullString
Exit Sub
CleanFail:
Set result = TestResult.Create(Inconclusive, "Test raised an error: " & Err.Description)
Resume CleanExit
End Sub
Private Function CurrentTestFailedResults() As List
Dim resultList As List
Set resultList = List.Create
Dim result As TestResult
For Each result In this.CurrentTestResults
If result.TestOutcome = Failed Then resultList.Add result
Next
Set CurrentTestFailedResults = resultList
End Function
Private Sub assertion_Completed(ByVal result As TestResult)
this.CurrentTestResults.Add result
If result.TestOutcome = Failed And CurrentTestFailedResults.Count = 0 Then
this.Output.WriteResult this.CurrentTest, result
End If
End Sub
Private Sub Class_Initialize()
Set assertion = Assert.DefaultInstance
End Sub
声明类模块
Assert
类经过了彻底的简化:Public Event Completed(ByVal result As TestResult)
Option Explicit
Private Sub OnAssertSucceeded()
RaiseEvent Completed(TestResult.Create(Succeeded))
End Sub
Private Sub OnAssertFailed(ByVal name As String, ByVal message As String)
RaiseEvent Completed(TestResult.Create(Failed, name & " failed: " & message))
End Sub
Private Sub OnAssertInconclusive(ByVal message As String)
RaiseEvent Completed(TestResult.Create(0, message))
End Sub
Public Property Get DefaultInstance() As Assert
Set DefaultInstance = Me
End Property
Public Sub IsTrue(ByVal condition As Boolean, Optional ByVal message As String)
If condition Then
OnAssertSucceeded
Else
OnAssertFailed "IsTrue", message
End If
End Sub
Public Sub IsFalse(ByVal condition As Boolean, Optional ByVal message As String)
If Not condition Then
OnAssertSucceeded
Else
OnAssertFailed "IsFalse", message
End If
End Sub
Public Sub Inconclusive(Optional ByVal message As String)
OnAssertInconclusive message
End Sub
Public Sub Fail(Optional ByVal message As String)
OnAssertFailed "Fail", message
End Sub
Public Sub IsNothing(ByVal value As Object, Optional ByVal message As String)
If value Is Nothing Then
OnAssertSucceeded
Else
OnAssertFailed "IsNothing", message
End If
End Sub
Public Sub IsNotNothing(ByVal value As Object, Optional ByVal message As String)
If Not value Is Nothing Then
OnAssertSucceeded
Else
OnAssertFailed "IsNotNothing", message
End If
End Sub
Public Sub AreEqual(ByVal value1 As Variant, ByVal value2 As Variant, Optional ByVal message As String)
Dim result As Boolean
result = (value1 = value2)
If IsObject(value1) And IsObject(value2) Then
If TypeOf value1 Is IEquatable And TypeOf value2 Is IEquatable Then
Dim equatable1 As IEquatable
Set equatable1 = value1
Dim equatable2 As IEquatable
Set equatable2 = value2
result = equatable1.Equals(equatable2)
End If
End If
If result Then
OnAssertSucceeded
Else
OnAssertFailed "AreEqual", message
End If
End Sub
Public Sub AreNotEqual(ByVal value1 As Variant, ByVal value2 As Variant, Optional ByVal message As String)
Dim result As Boolean
result = (value1 = value2)
If IsObject(value1) And IsObject(value2) Then
If TypeOf value1 Is IEquatable And TypeOf value2 Is IEquatable Then
Dim equatable1 As IEquatable
Set equatable1 = value1
Dim equatable2 As IEquatable
Set equatable2 = value2
result = equatable1.Equals(equatable2)
End If
End If
If Not result Then
OnAssertSucceeded
Else
OnAssertFailed "AreNotEqual", message
End If
End Sub
Public Sub AreSame(ByVal value1 As Object, ByVal value2 As Object, Optional ByVal message As String)
If (ObjPtr(value1) = ObjPtr(value2)) Then
OnAssertSucceeded
Else
OnAssertFailed "AreSame", message
End If
End Sub
Public Sub AreNotSame(ByVal value1 As Object, ByVal value2 As Object, Optional ByVal message As String)
If Not (ObjPtr(value1) = ObjPtr(value2)) Then
OnAssertSucceeded
Else
OnAssertFailed "AreNotSame", message
End If
End Sub
3。反射
我不是故意这么做的。我要怪@RubberDuck的这个好主意。我不想要求客户端代码降低其安全性设置,但是在这种情况下,好处显然超过了“风险”。
这里有点元编程,并且这是我第一次认真使用VBE。当然可以在这里有所改善。
ClassModule类模块
此类几乎是一个帮助程序,它获取
CodeModule
的方法成员列表。实例。现在。VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "ClassModule"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = True
Option Explicit
Public Function GetMethods(ByVal projectName As String, ByVal className As String) As List
Dim result As List
Set result = List.Create
Dim procedureName As String, lastFound As String
Dim procedureBody As String
Dim proj As VBProject
Set proj = GetProject(projectName)
If proj Is Nothing Then Exit Function
Dim module As CodeModule
Set module = GetClass(proj, className)
Dim i As Long
For i = module.CountOfDeclarationLines + 1 To module.CountOfLines
procedureName = module.ProcOfLine(i, vbext_pk_Proc)
If procedureName <> lastFound Then
procedureBody = module.Lines(module.ProcStartLine(procedureName, vbext_pk_Proc), module.ProcCountLines(procedureName, vbext_pk_Proc))
result.Add Method.Create(procedureName, procedureBody)
lastFound = procedureName
End If
Next
Set GetMethods = result
End Function
Private Function GetProject(ByVal projectName As String) As VBProject
Dim proj As VBProject
For Each proj In Application.VBE.VBProjects
If proj.Name = projectName Then
Set GetProject = proj
Exit Function
End If
Next
End Function
Private Function GetClass(ByVal project As VBProject, ByVal className As String) As CodeModule
Dim component As VBComponent
For Each component In project.VBComponents
If component.Name = className Then
Set GetClass = component.CodeModule
Exit Function
End If
Next
End Function
方法类模块
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "Method"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = True
Private Type TMethod
Name As String
Body As String
End Type
Private this As TMethod
Option Explicit
Public Property Get Name() As String
Name = this.Name
End Property
Friend Property Let Name(ByVal value As String)
this.Name = value
End Property
Public Property Get Body() As String
Body = this.Body
End Property
Friend Property Let Body(ByVal value As String)
this.Body = value
End Property
Public Function Create(ByVal methodName As String, ByVal methodBody As String) As Method
Dim result As New Method
result.Name = methodName
result.Body = methodBody
Set Create = result
End Function
/>
这肯定可以改善。怎么样?
#1 楼
看看我发现了什么Dim procedureName As String, lastFound As String
Dim procedureBody As String
我个人不喜欢这样声明变量,几乎每种语言都允许您以某种方式执行此操作。
我认为这是那些神圣的战争问题之一,有些程序员喜欢这样做,有些程序员则说这是不好的做法。
警惕要维护的人这段代码以及当您离开时他们会怎么说。
我发现了其他内容,可能是更改代码或逻辑的遗物
Private Function CanAddTestMethod(ByVal prospect As Method, ByRef testMethods As Dictionary) As Boolean
Dim result As Boolean
If Not IsTestMethodName(prospect) Then Exit Function
If testMethods.Exists(prospect.name) Then Exit Function
CanAddTestMethod = True
End Function
我猜想您不再需要
Dim result As Boolean
,它就剩了,尽管我认为您可能想要这样的东西
Private Function CanAddTestMethod(ByVal prospect As Method, ByRef testMethods As Dictionary) As Boolean
CanAddTestMethod = false
If Not IsTestMethodName(prospect) Then Exit Function
If testMethods.Exists(prospect.name) Then Exit Function
CanAddTestMethod = True
End Function
因此,如果您在其中一个if语句中退出该函数,则它说“嘿,我是假的,您不能添加测试方法”。
,但是我记得我们在这里谈论的是VBA,我认为如果您不设置方法就退出,它将自动为假,因此它只是
Private Function CanAddTestMethod(ByVal prospect As Method, ByRef testMethods As Dictionary) As Boolean
If Not IsTestMethodName(prospect) Then Exit Function
If testMethods.Exists(prospect.name) Then Exit Function
CanAddTestMethod = True
End Function
在
RunTest
子目录中,您等待直到第三行代码声明错误handlin g,我不确定这是否是故意的,如果GoTo较早并且在第三行之前出现错误,则错误说明可能无法正常工作。
想一想。
我们可以将
Exit Function
函数中的GetMethods
代码移动到该函数的开头,这样就不必使变量变暗了吗? 调试代码时会更容易看到
而不是This
Public Function GetMethods(ByVal projectName As String, ByVal className As String) As List
Dim result As List
Set result = List.Create
Dim procedureName As String, lastFound As String
Dim procedureBody As String
Dim proj As VBProject
Set proj = GetProject(projectName)
If proj Is Nothing Then Exit Function
Dim module As CodeModule
Set module = GetClass(proj, className)
Dim i As Long
For i = module.CountOfDeclarationLines + 1 To module.CountOfLines
procedureName = module.ProcOfLine(i, vbext_pk_Proc)
If procedureName <> lastFound Then
procedureBody = module.Lines(module.ProcStartLine(procedureName, vbext_pk_Proc), module.ProcCountLines(procedureName, vbext_pk_Proc))
result.Add Method.Create(procedureName, procedureBody)
lastFound = procedureName
End If
Next
Set GetMethods = result
End Function
编写它像这样
Public Function GetMethods(ByVal projectName As String, ByVal className As String) As List
Dim proj As VBProject
Set proj = GetProject(projectName)
If proj Is Nothing Then Exit Function
Dim result As List
Set result = List.Create
Dim procedureName As String, lastFound As String
Dim procedureBody As String
Dim module As CodeModule
Set module = GetClass(proj, className)
Dim i As Long
For i = module.CountOfDeclarationLines + 1 To module.CountOfLines
procedureName = module.ProcOfLine(i, vbext_pk_Proc)
If procedureName <> lastFound Then
procedureBody = module.Lines(module.ProcStartLine(procedureName, vbext_pk_Proc), module.ProcCountLines(procedureName, vbext_pk_Proc))
result.Add Method.Create(procedureName, procedureBody)
lastFound = procedureName
End If
Next
Set GetMethods = result
End Function
所有的括号和分号在哪里?
#2 楼
我还没有真正深入研究这里的内容,也许其他人会为您提供有关单元测试代码的全新视角。我想解决ClassModule.GetMethods
中的算法问题。我对这种效率低下负有部分责任,因为在先前的回答中,我向您指出了我之前编写的一些代码。 您当前使用的算法是\ $ O(n)\ $,其中\ $ N \ $是模块中的行数。通过切换到
while
循环并直接查找下一个方法的行号,可以在\ $ O(log n)\ $时间内完成此操作。所以,而不是这样:
Dim i As Long
For i = module.CountOfDeclarationLines + 1 To module.CountOfLines
procedureName = module.ProcOfLine(i, vbext_pk_Proc)
If procedureName <> lastFound Then
procedureBody = module.Lines(module.ProcStartLine(procedureName, vbext_pk_Proc), module.ProcCountLines(procedureName, vbext_pk_Proc))
result.Add Method.Create(procedureName, procedureBody)
lastFound = procedureName
End If
Next
使用此:
Dim lineNumber as Long
lineNumber = module.CountOfDeclarationLines + 1
While (lineNumber < module.CountOfLines)
procedureName = module.ProcOfLine(lineNumber, vbext_pk_Proc)
procedureBody = module.Lines(module.ProcStartLine(procedureName, vbext_pk_Proc), module.ProcCountLines(procedureName, vbext_pk_Proc))
result.Add Method.Create(procedureName, procedureBody)
' add current start line to the number of lines in current procedure to get the start line of the next procedure.
lineNumber = lineNumber + module.ProcCountLines(procedureName, vbext_pk_Proc) + 1
Wend
考虑到这一点,我不是这行代码的忠实拥护者,在上面的改进算法版本中,我并没有更改。
procedureBody = module.Lines(module.ProcStartLine(procedureName, vbext_pk_Proc), module.ProcCountLines(procedureName, vbext_pk_Proc))
这不是你的错。可扩展性库非常笨拙。这就是我扩展它的原因。我不太了解的是为什么您创建了
Method
类,但是没有实现使这些类型的调用更容易的任何事情。特别是StartLine
,CountOfLines
,EndLine
和Body
。 (实际上,您是在我的库中编写了许多属性的人。)请考虑vbeProcedure
(方法)具有这些属性的等效代码。Dim lineNumber As Long
lineNumber = codeMod.CountOfDeclarationLines + 1
While (lineNumber < codeMod.CountOfLines)
procName = codeMod.ProcOfLine(lineNumber, vbext_pk_Proc)
Set proc = New vbeProcedure
proc.Initialize procName, codeMod
procs.Add proc
lineNumber = proc.EndLine + 1
Wend
Set GetProcedures = procs
当然,这样做意味着每个Method都必须知道它属于哪个父模块,因此,如果采用这种方法,请小心使用循环引用,并确保正确处置
Method
。 > GetProject
过于复杂。这就是您真正需要的。Private Function GetProject(ByVal projectName As String) As VBProject
GetProject = Application.VBE.VBProjects(projectName)
End Function
当然,如果您传递一个空字符串或一个不存在的名称,它将炸毁,但这是一件好事。您的代码也将崩溃,它将在堆栈跟踪的更深处执行,您将不知道为什么
Project Is Nothing
。让它尽早失败,尤其是当您在使用元编程进行混乱时。 从头开始。我看到您在其中添加了一些代码。
If proj Is Nothing Then Exit Function
但是我还是不喜欢它。在一周的任何一天,我都希望有一条错误消息而不是无声的失败。
评论
\ $ \ begingroup \ $
好的帖子,但是我想问,调用Application.VBE.VBProjects(projectName)是否比通过Application.VBE.VBProjects进行迭代更安全。根据我在VBE方面的经验,通常不会收到错误消息或无提示的故障。它们中的任何一种都有不可预测的趋势来删除VBA主机。我不知道在最新版本的Office中这种情况是否有所改善,但我对此表示怀疑。我想说这是一个好习惯,当您在代码运行时所处的上下文中进行错误处理时,请尽可能避免发生错误。
\ $ \ endgroup \ $
–共产国际
2014年9月17日下午2:12
\ $ \ begingroup \ $
我从来没有暗示过@Comintern会更安全。我只是认为对于维护者而言,这将减少混乱。
\ $ \ endgroup \ $
–RubberDuck
2014年9月17日下午13:12
#3 楼
IsTestMethodName
很糟糕。该函数应该是IsTestMethod
,并且可以使用一些常量:Private Const TestMethodNamePrefix As String = "Test"
Private Const TestMethodAttribute As String = "TestMethod"
在
Reflection.Method
类中提供更多属性和帮助函数:Friend Property Let Body(ByVal value As String)
this.Body = value
FindSignature
End Property
Public Property Get Signature() As String
Signature = this.Signature
End Property
'Private Const MethodAttributeMarker As String = "'@"
Public Function HasAttribute(ByVal value As String) As Boolean
HasAttribute = this.AttributeComment = MethodAttributeMarker & Trim(value)
End Function
Private Sub FindSignature()
Dim lines() As String
lines = Split(this.Body, vbNewLine)
Dim i As Integer
For i = LBound(lines) To UBound(lines)
If framework.Strings.StartsWithAny(lines(i), False, "public sub", _
"private sub", _
"friend sub", _
"sub", _
"public function", _
"private function", _
"friend function", _
"function") _
Then
this.Signature = lines(i)
If i > 0 Then
If framework.Strings.StartsWith(AttributeMarker, lines(i - 1)) Then
this.AttributeComment = lines(i - 1)
End If
End If
Exit Sub
End If
Next
End Sub
...您可以一时兴起将
IsTestMethodName
转换为IsTestMethod
。当前实现的一个问题是对Body
行索引的硬编码-代码假定它会在第二行(索引1)找到一个'@TestMethod
“属性”。.但是过程的主体并不以其签名开头:它开始于VBA编辑器使用水平规则标记的那一行:客户端代码很可能在方法之间有5条空行,这会破坏您的代码。因此,鉴于上述属性,
IsTestMethod
看起来像这样:Private Function IsTestMethod(ByVal testMethod As Method) As Boolean
Dim result As Boolean
result = _
Framework.Strings.StartsWith(TestMethodNamePrefix, testMethod.name, False) Or _
testMethod.HasAttribute(TestMethodAttribute)
If Not result Then Exit Function
result = result And Framework.Strings.StartsWith("public sub", testMethod.Signature, False)
IsTestMethod = result
End Function
现在,如果
testMethod.Name
,testMethod.AttributeComment
或testMethod.Signature
的值存在问题,则该代码中没有该错误-它可以仅在Reflection
代码中。#4 楼
BUG!,我简直不敢相信这一点:
2014-09-16 00:24:20 TestInconclusive: [PASS]
TestInconclusive
测试方法应输出以下内容:2014-09-16 00:24:20 TestInconclusive: [INCONCLUSIVE] - No idea.
TestEngine.CurrentTestFailedResults()
函数可以重命名为CurrentTestNotPassedResults()
,并考虑到Inconclusive
的结果:For Each result In this.CurrentTestResults
If result.TestOutcome = Failed Or result.TestOutcome = Inconclusive Then resultList.Add result
Next
,然后可以将
assertion_Completed
处理程序修改为:Private Sub assertion_Completed(ByVal result As TestResult)
this.CurrentTestResults.Add result
If (result.TestOutcome = Inconclusive Or result.TestOutcome = Failed) _
And CurrentTestNotPassedResults.Count = 0 _
Then
this.Output.WriteResult this.CurrentTest, result
End If
End Sub
修复了错误并进行了测试:
Public Sub TestInconclusive()
assert.IsTrue True
assert.Inconclusive "No idea."
assert.Fail "shouldn't output this result."
End Sub
输出此单个结果,因为它应该:
2014-09-21 12:29:01 TestInconclusive: [INCONCLUSIVE] - No idea.
元组
它修复了该错误,但是仍然有一些气味:
RegisteredTests
私有成员是Dictionary<string,Tuple>
:For Each prospect In classMethods
If CanAddTestMethod(prospect, result) Then
result.Add prospect.name, Tuple.Create(classInstance, prospect.name)
Debug.Print "Registered test: " & TypeName(classInstance) & "." & prospect.name
End If
Next
这导致这行代码-事实证明,这是所有代码中最重要的一行,其内容如下:
CallByName test.Item1, test.Item2, VbMethod
Item1
和Item2
都是Variant
,绝对没有任何意义。与.NET代码一样,使用Tuple
表示缺少抽象-这里是一个简单的TestMethod
类: If CanAddTestMethod(prospect, result) Then
result.Add prospect.name, TestMethod.Create(classInstance, prospect.name)
Debug.Print "Registered test: " & TypeName(classInstance) & "." & prospect.name
End If
这还不很清楚吗?
Dim test As TestMethod
Set test = this.RegisteredTests(name)
CallByName test.OwnerInstance, test.MethodName, VbMethod
CurrentTestNotPassedResults()
此函数看起来像是拼凑而成的。它可以工作,但是闻起来很糟糕。而且它根本没有效率:每次执行测试时都会重新构建测试结果的“失败/不确定”列表(哎呀,每次在测试中进行断言),这意味着客户端测试类必须进行更多的测试运行,将花费更长的时间来处理以后的测试。不好。
TestEngine
确实会从拥有私有FailedOrInconclusiveResults
列表中受益,而不是使用以下功能:Private Type TTestEngine
Output As ITestOutput
RegisteredTests As Dictionary
CurrentTest As String
CurrentTestAllResults As List
CurrentTestFailedOrInconclusiveResults As List
End Type
然后可以简化
assert_Completed
处理程序:Private Sub assertion_Completed(ByVal result As TestResult)
this.CurrentTestAllResults.Add result
If result.TestOutcome = Inconclusive Or result.TestOutcome = Failed Then
this.CurrentTestFailedOrInconclusiveResults.Add result
End If
End Sub
在执行测试后,该命令将删除除一个
test.Output.WriteResult
之外的所有调用-剩下的仅一个在RunTest
方法中。更清洁。#5 楼
错误这是一个微妙的问题。
procedureBody = module.Lines(module.ProcStartLine(procedureName, vbext_pk_Proc), module.ProcCountLines(procedureName, vbext_pk_Proc))
具体来说,这是:
module.ProcStartLine(procedureName, vbext_pk_Proc)
只要您从不传递属性名称,它就可以100%罚款。公平地说,您是测试类,永远不要拥有属性,但是如果有的话……。天哪!当心!运行时错误#35正等待达到其丑陋的顶峰。
问题是
ProcStartLine
需要知道它正在按名称查找哪种方法。这是一个问题,因为以这种方式使用可扩展性库时,我们极不可能知道我们是在预先处理属性还是方法。 因此,在我关于\ $ O(log n)\ $解决方案的其他答案中,所有这些东西都忘了。为了安全地执行此操作,您必须逐行解析代码模块。这是我想出的解决方案。它不是很漂亮,但是可以使代码安全。
Private Function GetProcedureType(signatureLine As String) As vbext_ProcKind
If InStr(1, signatureLine, "Property Get") > 0 Then
GetProcedureType = vbext_pk_Get
ElseIf InStr(1, signatureLine, "Property Let") > 0 Then
GetProcedureType = vbext_pk_Let
ElseIf InStr(1, signatureLine, "Property Set") > 0 Then
GetProcedureType = vbext_pk_Set
ElseIf InStr(1, signatureLine, "Sub") > 0 Or InStr(1, signatureLine, "Function") > 0 Then
GetProcedureType = vbext_pk_Proc
Else
Const InvalidProcedureCallOrArgument As Long = 5
Err.Raise InvalidProcedureCallOrArgument
End If
End Function
Private Function IsSignature(line As String) As Boolean
If line = vbNullString Then Exit Function
Dim propertyPosition As Long
Dim functionPosition As Long
Dim subPosition As Long
Dim commentPosition As Long
propertyPosition = InStr(1, line, "Property")
functionPosition = InStr(1, line, "Function")
subPosition = InStr(1, line, "Sub")
commentPosition = InStr(1, line, "'")
If propertyPosition > 0 Or functionPosition > 0 Or subPosition > 0 Then
If InStr(1, line, "End") = 0 Then
If commentPosition > propertyPosition Then
Exit Function
ElseIf commentPosition > functionPosition Then
Exit Function
ElseIf commentPosition > subPosition Then
Exit Function
Else
IsSignature = True
End If
End If
End If
End Function
将其应用于代码看起来像这样。请注意,我将
procKind
添加为Method
的成员,并且无需再次检查此proc名称的最后一个proc名称,因为我们仅在签名行上才添加方法。 /> 当然,您可能还需要先检查
ProcKind
,然后再将其注册为TestMethod
。我怀疑CallByName
确实会在真正获得财产时不被告知要获得vbMethod
。旁注:您可能会发现有趣的是,知道
ProcOfLine
不会遭受此损害。据我所知,您可以传递任何一个vbext_ProcKind
,它将愉快地运行。我们已经在聊天中讨论了这一点,但是为了将来的读者起见,我在这里添加。毕竟,\ $ O(n)\ $解决方案是可能的。
ProcOfLine
会占用您抛出的所有vbext_ProcKind
的原因是它是一个OUT参数。 请注意,pprockind参数指示该行是否属于
子或函数过程,属性获取过程,属性Let
过程或属性集过程。若要确定某行所在的
过程类型,请将Long类型的变量传递给ProcOfLine
属性,然后检查该变量的值。
摘自2013 MS Access文档。这很容易被遗漏,因为Visual Basic加载项对象引用的2013 Office版本和VB6版本根本没有提及它。解决方案是简单地声明一个
procKind
变量,然后让它为您捕获procKind。Dim procKind As vbext_ProcKind
Dim i As Long
For i = Module.CountOfDeclarationLines + 1 To Module.CountOfLines
line = Module.Lines(i, 1)
If IsSignature(line) Then
procKind = GetProcedureType(line)
procedureName = Module.ProcOfLine(i, procKind)
procedureBody = Module.Lines(Module.ProcStartLine(procedureName, procKind), Module.ProcCountLines(procedureName, procKind))
result.Add Method.Create(procedureName, procedureBody, procKind)
End If
Next
评论
\ $ \ begingroup \ $
这里有两个不错的地方。但是,VBA不在乎Dim语句在哪里-如果在函数中声明了Dim语句,则它在该函数中的任何作用域内;您不能在Dim语句上打断:)
\ $ \ endgroup \ $
– Mathieu Guindon♦
2014-09-16 19:41
\ $ \ begingroup \ $
@ Mat'sMug再看一遍。如果在变暗之后退出函数,它已经使用了处理能力来变暗那些变量,如果您在那些变暗的语句之前退出了函数,它们是从未创建的,对吧?
\ $ \ endgroup \ $
–马拉奇♦
2014-09-16 19:46
\ $ \ begingroup \ $
是的。列表永远不会被创建,但是马克杯是正确的,因为一个作用域的Dims都同时发生。
\ $ \ endgroup \ $
–RubberDuck
2014年9月16日19:54
\ $ \ begingroup \ $
“括号和分号都在哪里?”使我的饮料从我的鼻子里冒出来。无论如何还是要进行+1观察。
\ $ \ endgroup \ $
–共产国际
2014年9月17日下午2:05
\ $ \ begingroup \ $
@RubberDuck,我现在明白了,在它们变暗之前退出并不重要,因为编译器/解释器/(无论VBA使用什么)都检查范围并从头开始设置内存,然后一直运行范围。我明白了!
\ $ \ endgroup \ $
–马拉奇♦
2014年9月17日下午13:33