使用同一数据库中的代码、表单和数据,我想知道为 Microsoft Access 应用程序(例如 Access 2007)设计一套测试的最佳实践是什么。
测试表单的主要问题之一是只有少数控件具有hwnd
句柄,而其他控件只有一个它们具有焦点,这使得自动化非常不透明,因为您无法在表单上获得要操作的控件列表。
有什么经验可以分享吗?
使用同一数据库中的代码、表单和数据,我想知道为 Microsoft 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 插件的单元测试框架
免责声明:我是共同开发者之一。
我有偏见,但这是迄今为止我最喜欢的一群。
所以,回到第 1 节中的代码。我们真正需要测试的唯一代码是MyModel.Reversed()
函数。那么,让我们来看看这个测试会是什么样子。(给出的示例使用 Rubberduck,但这是一个简单的测试,可以转化为您选择的框架。)
'@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 代码编写单元测试。
我很欣赏诺克斯和大卫的回答。我的答案将介于他们之间:只需制作不需要调试的表单!
我认为表单应该被排他性地使用它们基本上,仅意味着图形界面,这里意味着它们不必被调试!然后,调试作业仅限于您的 VBA 模块和对象,这更容易处理。
将 VBA 代码添加到表单和/或控件当然是一种自然趋势,特别是当 Access 为您提供这些出色的“更新后”和“更改时”事件时,但我绝对建议您不要放置任何表单或控件特定代码在表单的模块中。这使得进一步的维护和升级非常昂贵,您的代码在 VBA 模块和表单/控件模块之间拆分。
这并不意味着您不能再使用此AfterUpdate
活动!只需将标准代码放入事件中,如下所示:
Private Sub myControl_AfterUpdate()
CTLAfterUpdate myControl
On Error Resume Next
Eval ("CTLAfterUpdate_MyForm()")
On Error GoTo 0
End sub
在哪里:
CTLAfterUpdate
是每次在表单中更新控件时运行的标准过程
CTLAfterUpdateMyForm
是每次在 MyForm 上更新控件时运行的特定过程
然后我有2个模块。第一个是
utilityFormEvents
第二个是
MyAppFormEvents
选择这样一个通用的解决方案意味着很多。这意味着您正在达到高水平的代码规范化(意味着代码的无痛维护)。而当你说你没有任何特定于表单的代码时,这也意味着表单模块是完全标准化的,它们的生产可以自动化:只需说出你想在表单/控件级别管理哪些事件,并定义你的通用/特定程序术语。
编写您的自动化代码,一劳永逸。
这需要几天的工作,但它会产生令人兴奋的结果。在过去的 2 年里,我一直在使用这个解决方案,它显然是正确的:我的表单是完全自动从头开始创建的,带有一个链接到一个“控件表”的“表单表”。
然后,我可以花时间处理表格的具体程序(如果有的话)。
代码规范化,即使使用 MS Access,也是一个漫长的过程。但这真的是值得的痛苦!
Access 作为 COM 应用程序的另一个优点是您可以创建一个.NET 应用程序来通过 Automation 运行和测试 Access 应用程序。这样做的好处是,您可以使用更强大的测试框架(例如NUnit)来编写针对 Access 应用程序的自动断言测试。
因此,如果您精通 C# 或 VB.NET 以及 NUnit 之类的东西,那么您可以更轻松地为您的 Access 应用程序创建更大的测试覆盖率。
我从Python 的 doctest概念中提取了一页,并在 Access VBA 中实现了一个 DocTests 过程。这显然不是一个成熟的单元测试解决方案。它还比较年轻,所以我怀疑我已经解决了所有的错误,但我认为它已经足够成熟,可以放生了。
只需将以下代码复制到标准代码模块中,然后在 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.Application 对象模型中的一个函数;这意味着您可以在 Access 之外使用它,但需要创建一个 Access.Application 对象并完全限定Eval
调用Eval
需要注意尽管有其局限性,但我仍然认为它为您提供了相当大的收益。
编辑:这是一个简单的函数,该函数必须满足“doctest rules”。
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
虽然这是一个非常古老的答案:
有AccUnit,一个用于 Microsoft Access 的专用单元测试框架。
我会将应用程序设计为在查询和 vba 子例程中完成尽可能多的工作,以便您的测试可以由填充测试数据库、针对这些数据库运行生产查询和 vba 集然后查看输出和比较以确保输出良好。这种方法显然不会测试 GUI,因此您可以使用手动执行的一系列测试脚本(这里我的意思是像打开表单 1 并单击控件 1 的 word 文档)来增加测试。
它取决于项目的范围作为测试方面所需的自动化水平。
如果您有兴趣在更精细的级别测试您的 Access 应用程序,特别是 VBA 代码本身,那么VB Lite Unit是一个很好的单元测试框架。
这里有很好的建议,但我很惊讶没有人提到集中错误处理。您可以获得允许快速函数/子模板和添加行号的插件(我使用 MZ 工具)。然后将所有错误发送到一个可以记录它们的函数。然后,您还可以通过设置单个断点来中断所有错误。
我发现在我的应用程序中进行单元测试的机会相对较少。我编写的大多数代码都与表数据或文件系统交互,因此基本上很难进行单元测试。早期,我尝试了一种可能类似于模拟(欺骗)的方法,其中我创建了具有可选参数的代码。如果使用了该参数,则该过程将使用该参数而不是从数据库中获取数据。设置与一行数据具有相同字段类型的用户定义类型并将其传递给函数非常容易。我现在有一种方法可以将测试数据放入我想要测试的过程中。在每个过程中都有一些代码将真实数据源替换为测试数据源。这让我可以对更广泛的功能使用单元测试,使用我自己的单元测试功能。编写单元测试很容易,只是重复和无聊。最后,我放弃了单元测试并开始使用不同的方法。
我主要为自己编写内部应用程序,这样我就可以等到问题找到我,而不必拥有完美的代码。如果我确实为客户编写应用程序,通常客户并不完全了解软件开发成本有多少,因此我需要一种低成本的方式来获得结果。编写单元测试就是编写一个测试,将坏数据推送到程序中,以查看程序是否可以适当地处理它。单元测试还确认正确处理了良好的数据。我目前的方法是基于将输入验证写入应用程序中的每个过程,并在代码成功完成时引发成功标志。每个调用过程在使用结果之前都会检查成功标志。如果出现问题,将通过错误消息进行报告。每个函数都有一个成功标志,返回值、错误消息、注释和来源。用户定义类型(fr 表示函数返回)包含数据成员。任何给定的函数都只填充用户定义类型中的一些数据成员。当一个函数运行时,它通常返回success = true 和一个返回值,有时还有一个注释。如果函数失败,则返回 success = false 和错误消息。如果函数链失败,错误消息会以菊花形式更改,但结果实际上比正常的堆栈跟踪更具可读性。起源也被链接起来,所以我知道问题发生在哪里。该应用程序很少崩溃并准确报告任何问题。结果比标准错误处理要好得多。用户定义类型(fr 表示函数返回)包含数据成员。任何给定的函数都只填充用户定义类型中的一些数据成员。当一个函数运行时,它通常返回success = true 和一个返回值,有时还有一个注释。如果函数失败,则返回 success = false 和错误消息。如果函数链失败,错误消息会以菊花形式更改,但结果实际上比正常的堆栈跟踪更具可读性。起源也被链接起来,所以我知道问题发生在哪里。该应用程序很少崩溃并准确报告任何问题。结果比标准错误处理要好得多。用户定义类型(fr 表示函数返回)包含数据成员。任何给定的函数都只填充用户定义类型中的一些数据成员。当一个函数运行时,它通常返回success = true 和一个返回值,有时还有一个注释。如果函数失败,则返回 success = false 和错误消息。如果函数链失败,错误消息会以菊花形式更改,但结果实际上比正常的堆栈跟踪更具可读性。起源也被链接起来,所以我知道问题发生在哪里。该应用程序很少崩溃并准确报告任何问题。结果比标准错误处理要好得多。它通常返回 success = true 和返回值,有时还返回注释。如果函数失败,则返回 success = false 和错误消息。如果函数链失败,错误消息会以菊花形式更改,但结果实际上比正常的堆栈跟踪更具可读性。起源也被链接起来,所以我知道问题发生在哪里。该应用程序很少崩溃并准确报告任何问题。结果比标准错误处理要好得多。它通常返回 success = true 和返回值,有时还返回注释。如果函数失败,则返回 success = false 和错误消息。如果函数链失败,错误消息会以菊花形式更改,但结果实际上比正常的堆栈跟踪更具可读性。起源也被链接起来,所以我知道问题发生在哪里。该应用程序很少崩溃并准确报告任何问题。结果比标准错误处理要好得多。该应用程序很少崩溃并准确报告任何问题。结果比标准错误处理要好得多。该应用程序很少崩溃并准确报告任何问题。结果比标准错误处理要好得多。
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
代码解释。eOutputFolder 是用户定义的枚举,如下所示
Public Enum eOutputFolder
eDefaultDirectory = 1
eAppPath = 2
eCustomPath = 3
End Enum
我正在使用 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
用户定义的类型(例如 FunctRet)也提供了帮助的代码完成。在过程中,我通常将内部结果存储到匿名内部变量 (fr),然后再将结果分配给返回变量 (GetOutputFolder)。这使得重命名过程非常容易,因为只更改了顶部和底部。
综上所述,我开发了一个带有 ms-access 的框架,涵盖了所有涉及 VBA 的操作。测试被永久写入程序,而不是开发时的单元测试。在实践中,代码仍然运行得非常快。我非常小心地优化了每分钟可以调用一万次的低级函数。此外,我可以在生产中使用正在开发的代码。如果发生错误,它是用户友好的,并且错误的来源和原因通常是显而易见的。错误是从调用表单报告的,而不是从业务层的某个模块报告的,这是应用程序设计的一个重要原则。此外,我没有维护单元测试代码的负担,这在我改进设计而不是编写清晰概念化的设计时非常重要。
有一些潜在的问题。测试不是自动化的,只有在应用程序运行时才会检测到新的错误代码。该代码看起来不像标准 VBA 代码(通常更短)。尽管如此,该方法仍有一些优势。使用错误处理程序来记录错误要好得多,因为用户通常会联系我并给我一个有意义的错误消息。它还可以处理处理外部数据的过程。JavaScript 让我想起了 VBA,我想知道为什么 JavaScript 是框架之乡,而 ms-access 中的 VBA 不是。
写完这篇文章几天后,我发现一篇关于 CodeProject的文章与我上面写的很接近。文章比较和对比了异常处理和错误处理。我上面的建议类似于异常处理。
我还没有尝试过,但您可以尝试将访问表单作为数据访问网页发布到共享点或网页等内容,然后使用selenium等工具通过一组测试来驱动浏览器。
显然,这并不像直接通过单元测试驱动代码那么理想,但它可能会让你成为其中的一部分。祝你好运
Access 是一个 COM 应用程序。使用 COM,而不是 Windows API。在 Access 中测试东西。
Access 应用程序的最佳测试环境是 Access。您的所有表单/报告/表格/代码/查询都可用,有一种类似于 MS Test 的脚本语言(好吧,您可能不记得 MS Test),有用于保存您的测试脚本和测试结果的数据库环境,你在这里建立的技能可以转移到你的应用程序中。
数据访问页面已经被 MS 弃用了很长一段时间,并且从一开始就没有真正起作用(它们依赖于安装的 Office 小部件,并且只能在 IE 中工作,而且当时情况很糟糕)。
确实,可以获取焦点的 Access 控件只有在获得焦点时才具有窗口句柄(而那些无法获取焦点的控件,例如标签,则根本没有窗口句柄)。这使得 Access 非常不适合窗口句柄驱动的测试机制。
事实上,我质疑你为什么要在 Access 中进行这种测试。这听起来像是您的基本极限编程教条,并不是 XP 的所有原则和实践都适用于 Access 应用程序——方钉、圆孔。
因此,退后一步,问问自己您想要完成什么,并考虑您可能需要使用完全不同的方法,而不是那些基于无法在 Access 中工作的方法的方法。
或者这种自动化测试是否完全有效,甚至对 Access 应用程序是否有用。