使用同一数据库中的代码,表单和数据,我想知道为Microsoft Access应用程序设计一套测试的最佳实践是什么(比如Access 2007).
测试表单的主要问题之一是,只有少数控件具有hwnd
句柄,而其他控件只能获得一个焦点,这使得自动化非常不透明,因为您无法获取表单上的控件列表.
有经验可以分享吗?
首先,停止将业务逻辑写入您的Form的代码中.那不是它的地方.它无法在那里进行适当的测试.事实上,你真的不应该自己测试你的表单.这应该是响应用户交互,然后响应这些动作到另一个类,委托责任死哑巴简单的观点是测试.
你是怎样做的?熟悉模型 - 视图 - 控制器模式是一个良好的开端.
它不能在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
.这就是所有真实逻辑的所在.
所以,继续第二步.
2.选择单元测试框架这里没有很多选择.大多数框架都需要安装COM加载项,大量的样板,奇怪的语法,写测试作为评论等等.这就是为什么我自己参与构建一个,所以这部分答案不公正,但我会尝试对可用的内容进行公平的总结.
AccUnit
仅适用于Access.
要求您将测试编写为注释和代码的奇怪组合.(评论部分没有智能感知.
这里是一个图形界面,以帮助你写那些奇怪的看着测试虽然.
该项目自2013年以来未见任何更新.
VB Lite Unit 我不能说我亲自使用它.它在那里,但自2005年以来没有看到更新.
xlUnit xlUnit并不糟糕,但它也不好.它很笨重,还有很多锅炉板代码.这是最糟糕的,但它在Access中不起作用.所以,那就是了.
建立自己的框架
我去过那里并做到了.它可能比大多数人想要进入的更多,但完全有可能在Native VBA代码中构建单元测试框架.
Rubberduck VBE Add-In的单元测试框架
免责声明:我是其中一个共同开发者.
我有偏见,但这是迄今为止我最喜欢的一群.
很少甚至没有锅炉板代码.
Intellisense可用.
该项目很活跃.
比大多数这些项目更多的文档.
它适用于大多数主要的办公应用程序,而不仅仅是Access.
遗憾的是,它是一个COM加载项,因此必须安装到您的计算机上.
所以,回到第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
我将在哪里获得CTLAfterUpdate通用事件
第二个是
MyAppFormEvents
包含MyApp应用程序的所有特定形式的特定代码,并包括CTLAfterUpdateMyForm过程.当然,如果没有特定的代码可以运行,CTLAfterUpdateMyForm可能不存在.这就是为什么我们将"On error"变为"resume next"......
选择这样的通用解决方案意味着很多.这意味着您正在达到高级别的代码规范化(意味着无痛的代码维护).当你说你没有任何特定于表单的代码时,它也意味着表单模块是完全标准化的,并且它们的生产可以自动化:只需说明你想在表单/控件级别管理哪些事件,并定义你的通用/特定程序术语.
一劳永逸地编写自动化代码.
这需要几天的工作,但它会带来令人兴奋的结果.在过去的两年里,我一直在使用这个解决方案,它显然是正确的:我的表单是从头开始完全自动创建的"表格表",链接到"控制表".
然后,我可以花时间研究表单的特定过程(如果有的话).
即使使用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
一些快速说明:
它没有依赖关系(从Access中使用时)
它使用的Eval
是Access.Application对象模型中的一个函数; 这意味着您可以在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
虽然这是一个非常古老的答案:
有AccUnit,一个专门的Microsoft Access单元测试框架.