在同一个数据库中,代码、表单和数据都在一起的情况下,我想知道为微软Access应用程序(例如Access 2007)设计测试套件的最佳实践是什么。
测试表单的主要问题之一是只有少数控件具有hwnd
句柄,其他控件只有在获得焦点后才会有句柄。这使得自动化变得非常不透明,因为您无法获取表单上的控件列表并对其进行操作。
有没有经验可以分享?
在同一个数据库中,代码、表单和数据都在一起的情况下,我想知道为微软Access应用程序(例如Access 2007)设计测试套件的最佳实践是什么。
测试表单的主要问题之一是只有少数控件具有hwnd
句柄,其他控件只有在获得焦点后才会有句柄。这使得自动化变得非常不透明,因为您无法获取表单上的控件列表并对其进行操作。
有没有经验可以分享?
首先,停止在您的表单代码后面编写业务逻辑。那里不是它的位置,因为它无法得到适当的测试。实际上,你真的不应该测试你的表单本身。它应该是一个简单而死板的视图,只响应用户交互,然后将响应这些操作的责任委托给另一个可以进行测试的类。
怎么做呢?熟悉模型-视图-控制器模式是一个好的开始。
由于我们只有事件或接口中的一个,所以在VBA中无法完全做到,但您可以做得相当接近。考虑这个简单的表单,它有一个文本框和一个按钮。
在表单的代码后面,我们将在一个公共属性中包装TextBox的值,并重新引发我们感兴趣的任何事件。
Public Event OnSayHello()
Public Event AfterTextUpdate()
Public Property Let Text(value As String)
Me.TextBox1.value = value
End Property
Public Property Get Text() As String
Text = Me.TextBox1.value
End Property
Private Sub SayHello_Click()
RaiseEvent OnSayHello
End Sub
Private Sub TextBox1_AfterUpdate()
RaiseEvent AfterTextUpdate
End Sub
现在我们需要一个模型来使用。我已经创建了一个名为MyModel
的新类模块。这里是我们将要测试的代码。请注意,它自然地与我们的视图共享类似的结构。
Private mText As String
Public Property Let Text(value As String)
mText = value
End Property
Public Property Get Text() As String
Text = mText
End Property
Public Function Reversed() As String
Dim result As String
Dim length As Long
length = Len(mText)
Dim i As Long
For i = 0 To length - 1
result = result + Mid(mText, (length - i), 1)
Next i
Reversed = result
End Function
Public Sub SayHello()
MsgBox Reversed()
End Sub
最后,我们的控制器将所有内容连接在一起。 控制器侦听表单事件,并将更改通知给模型并触发模型的例程。
Private WithEvents view As Form_Form1
Private model As MyModel
Public Sub Run()
Set model = New MyModel
Set view = New Form_Form1
view.Visible = True
End Sub
Private Sub view_AfterTextUpdate()
model.Text = view.Text
End Sub
Private Sub view_OnSayHello()
model.SayHello
view.Text = model.Reversed()
End Sub
现在这段代码可以从任何其他模块中运行。为了这个例子的目的,我使用了一个标准模块。我强烈鼓励您使用我提供的代码构建它并查看它的功能。
Private controller As FormController
Public Sub Run()
Set controller = New FormController
controller.Run
End Sub
那很好,但这与测试有什么关系?!朋友,这与测试有一切的关系。我们所做的是使我们的代码可以进行测试。在我提供的示例中,甚至没有理由尝试测试GUI。我们真正需要测试的是model
。那里才是所有真正逻辑所在。
进入第二步。
这里没有很多选择。大多数框架需要安装COM插件,大量的样板文件,奇怪的语法,将测试作为注释编写等等。这就是为什么我参与自己构建一个框架,所以我的回答这部分不是客观的,但我会尽力给出一个公正的总结。
VB Lite Unit。我个人没用过它。它存在,但自2005年以来没有更新。
xlUnit。xlUnit不是很糟糕,但也不是很好。它很笨重,有很多样板文件。它是最好的选择,但在Access中无法使用。所以就此打住。
构建您自己的框架
我曾经走过这条路。这可能超出了大多数人想要涉足的范围,但完全有可能在原生VBA代码中构建一个单元测试框架。
Rubberduck VBE Add-In's单元测试框架
免责声明:我是其中一位合作开发者.
我有偏见,但这是我最喜欢的选择。
'@TestModule
Private Assert As New Rubberduck.AssertClass
'@TestMethod
Public Sub ReversedReversesCorrectly()
Arrange:
Dim model As New MyModel
Const original As String = "Hello"
Const expected As String = "olleH"
Dim actual As String
model.Text = original
Act:
actual = model.Reversed
Assert:
Assert.AreEqual expected, actual
End Sub
我知道答案有点长,并且晚了一些,但希望能帮助一些人开始为他们的VBA代码编写单元测试。
我很欣赏Knox和David的回答。我的答案将介于他们之间:只需制作不需要调试的表单!
我认为表单应该仅作为其基本用途,即图形界面而已,这意味着它们不必被调试!然后,调试工作仅限于您的VBA模块和对象,这要容易得多。
当然,有一种自然倾向于向表单和/或控件添加VBA代码,特别是当Access为您提供这些出色的“更新后”和“更改时”事件时,但我绝对建议您不要在表单的模块中放置任何特定于表单或控件的代码。这会使进一步的维护和升级非常昂贵,因为您的代码分为VBA模块和表单/控件模块。
这并不意味着您不能再使用AfterUpdate
事件!只需在事件中放入标准代码,如下所示:
Private Sub myControl_AfterUpdate()
CTLAfterUpdate myControl
On Error Resume Next
Eval ("CTLAfterUpdate_MyForm()")
On Error GoTo 0
End sub
Where:
CTLAfterUpdate
是在表单中每次控件更新时运行的标准过程。
CTLAfterUpdateMyForm
是在 MyForm 上每次控件更新时运行的特定过程。
然后我有两个模块。第一个是
utilityFormEvents
第二个是
MyAppFormEvents
选择这样的通用解决方案意义重大。这意味着您正在达到高水平的代码规范化(即无痛维护代码)。当您说您没有任何特定于表单的代码时,它还意味着表单模块完全标准化,它们的生产可以自动化:只需说明要管理的表单/控件级事件,并定义您的通用/特定过程术语。
编写一次自动化代码。
它需要几天的工作,但可以产生令人兴奋的结果。我过去两年一直在使用这个解决方案,它显然是正确的:我的表单完全是自动从“表单表”创建的,链接到“控件表”。
然后,如果需要,我可以花时间在表单的特定过程上。
即使在 MS Access 中,代码规范化也是一个漫长的过程。但它真的值得努力!
因此,如果您精通 C# 或 VB.NET 并结合类似于 NUnit 的东西,那么您可以更轻松地为您的 Access 应用程序创建更大的测试覆盖率。
只需将以下代码复制到标准代码模块中,然后在Sub内按F5即可看到其效果:
'>>> 1 + 1
'2
'>>> 3 - 1
'0
Sub DocTests()
Dim Comp As Object, i As Long, CM As Object
Dim Expr As String, ExpectedResult As Variant, TestsPassed As Long, TestsFailed As Long
Dim Evaluation As Variant
For Each Comp In Application.VBE.ActiveVBProject.VBComponents
Set CM = Comp.CodeModule
For i = 1 To CM.CountOfLines
If Left(Trim(CM.Lines(i, 1)), 4) = "'>>>" Then
Expr = Trim(Mid(CM.Lines(i, 1), 5))
On Error Resume Next
Evaluation = Eval(Expr)
If Err.Number = 2425 And Comp.Type <> 1 Then
'The expression you entered has a function name that '' can't find.
'This is not surprising because we are not in a standard code module (Comp.Type <> 1).
'So we will just ignore it.
GoTo NextLine
ElseIf Err.Number <> 0 Then
Debug.Print Err.Number, Err.Description, Expr
GoTo NextLine
End If
On Error GoTo 0
ExpectedResult = Trim(Mid(CM.Lines(i + 1, 1), InStr(CM.Lines(i + 1, 1), "'") + 1))
Select Case ExpectedResult
Case "True": ExpectedResult = True
Case "False": ExpectedResult = False
Case "Null": ExpectedResult = Null
End Select
Select Case TypeName(Evaluation)
Case "Long", "Integer", "Short", "Byte", "Single", "Double", "Decimal", "Currency"
ExpectedResult = Eval(ExpectedResult)
Case "Date"
If IsDate(ExpectedResult) Then ExpectedResult = CDate(ExpectedResult)
End Select
If (Evaluation = ExpectedResult) Then
TestsPassed = TestsPassed + 1
ElseIf (IsNull(Evaluation) And IsNull(ExpectedResult)) Then
TestsPassed = TestsPassed + 1
Else
Debug.Print Comp.Name; ": "; Expr; " evaluates to: "; Evaluation; " Expected: "; ExpectedResult
TestsFailed = TestsFailed + 1
End If
End If
NextLine:
Next i
Next Comp
Debug.Print "Tests passed: "; TestsPassed; " of "; TestsPassed + TestsFailed
End Sub
从名为Module1的模块中复制、粘贴并运行上述代码会产生以下结果:
Module: 3 - 1 evaluates to: 2 Expected: 0
Tests passed: 1 of 2
几个快速注意事项:
Eval
函数;这意味着你可以在Access之外使用它,但需要创建一个Access.Application对象并完全限定Eval
调用Eval
相关的特殊情况需要注意尽管它有一些限制,但我仍然认为它提供了相当不错的性价比。
编辑:这里是一个带有“doctest规则”的简单函数。
Public Function AddTwoValues(ByVal p1 As Variant, _
ByVal p2 As Variant) As Variant
'>>> AddTwoValues(1,1)
'2
'>>> AddTwoValues(1,1) = 1
'False
'>>> AddTwoValues(1,Null)
'Null
'>>> IsError(AddTwoValues(1,"foo"))
'True
On Error GoTo ErrorHandler
AddTwoValues = p1 + p2
ExitHere:
On Error GoTo 0
Exit Function
ErrorHandler:
AddTwoValues = CVErr(Err.Number)
GoTo ExitHere
End Function
测试方面所需的自动化水平取决于项目范围。
Public Function GetOutputFolder(OutputFolder As eOutputFolder) As FunctRet
'///Returns a full path when provided with a target folder alias. e.g. 'temp' folder
Dim fr As FunctRet
Select Case OutputFolder
Case 1
fr.Rtn = "C:\Temp\"
fr.Success = True
Case 2
fr.Rtn = TrailingSlash(Application.CurrentProject.path)
fr.Success = True
Case 3
fr.EM = "Can't set custom paths – not yet implemented"
Case Else
fr.EM = "Unrecognised output destination requested"
End Select
exitproc:
GetOutputFolder = fr
End Function
Public Enum eOutputFolder
eDefaultDirectory = 1
eAppPath = 2
eCustomPath = 3
End Enum
'Type FunctRet is used as a generic means of reporting function returns
Public Type FunctRet
Success As Long 'Boolean flag for success, boolean not used to avoid nulls
Rtn As Variant 'Return Value
EM As String 'Error message
Cmt As String 'Comments
Origin As String 'Originating procedure/function
End Type
这里有一些好的建议,但我很惊讶没有人提到集中式错误处理。您可以获取插件,允许快速添加函数/子模板和添加行号(我使用MZ-tools)。然后将所有错误发送到一个单独的函数中,您可以记录它们。您还可以通过设置单个断点来在所有错误时中断。