由于代码、表单和数据位于同一数据库中,我想知道为 Microsoft Access 应用程序(例如 Access 2007)设计一套测试的最佳实践是什么。

测试表单的主要问题之一是只有少数控件具有 hwnd 句柄和其他控件只能获得它们所关注的一个,这使得自动化非常不透明,因为您无法获取表单上要执行操作的控件列表。

有什么经验可以分享吗?

有帮助吗?

解决方案

1.编写可测试的代码

首先,停止将业务逻辑写入表单的代码后面。那不是它的地方。在那里无法对其进行正确的测试。事实上,您根本不必测试表单本身。它应该是一个非常愚蠢的简单视图,它响应用户交互,然后将响应这些操作的责任委托给另一个类,该类 可测试的。

你是怎样做的?熟悉 模型-视图-控制器模式 是一个好的开始。

Model View Controller diagram

这是不可能的 完美 在 VBA 中,由于我们要么获取事件,要么获取接口,而不是同时获取两者,但您可以非常接近。考虑这个简单的表单,它有一个文本框和一个按钮。

simple form with text box and button

在表单的代码后面,我们将 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 插件、大量样板、奇怪的语法、将测试编写为注释等。这就是我参与其中的原因 我自己建造一个, ,所以我的回答的这一部分并不公正,但我会尽力对可用的内容进行公平的总结。

  1. 账户单元

    • 仅适用于 Access。
    • 要求您将测试编写为注释和代码的奇怪混合体。(评论部分没有智能感知。
    • 那里 图形界面可以帮助您编写那些看起来很奇怪的测试。
    • 该项目自 2013 年以来没有任何更新。
  2. VB精简版单元我不能说我亲自使用过它。它已经存在,但自 2005 年以来就没有更新过。

  3. xl单位xlUnit 并不糟糕,但也不好。它很笨重,并且有很多样板代码。这是最坏中最好的,但它在 Access 中不起作用。所以,就这样了。

  4. 构建您自己的框架

    我有 去过那里并做过那件事. 。这可能超出了大多数人的想象,但完全有可能用本机 VBA 代码构建单元测试框架。

  5. Rubberduck VBE 插件的单元测试框架
    免责声明:我是联合开发者之一.

    我有偏见,但这是迄今为止我最喜欢的。

    • 很少甚至没有样板代码。
    • 智能感知可用。
    • 该项目正在积极进行中。
    • 比大多数这些项目更多的文档。
    • 它适用于大多数主要办公应用程序,而不仅仅是 Access。
    • 不幸的是,它是一个 COM 插件,因此必须将其安装到您的计算机上。

3.开始编写测试

那么,回到第 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

编写良好测试的指南

  1. 一次只测试一件事。
  2. 好的测试只有在系统出现错误或需求发生变化时才会失败。
  3. 请勿包含外部依赖项,例如数据库和文件系统。这些外部依赖项可能会导致测试因您无法控制的原因而失败。其次,它们会减慢你的测试速度。如果你的测试很慢,你就不会运行它们。
  4. 使用描述测试正在测试的内容的测试名称。如果时间变长,请不要担心。最重要的是它是描述性的。

我知道这个答案有点长,而且有点晚,但希望它可以帮助一些人开始为他们的 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 上更新控件时运行的特定过程

我有两个模块。第一个是

  • utilityFormEvents
    我将在其中进行 CTLAfterUpdate 通用事件

第二个是

  • MyAppFormEvents
    包含MyApp应用程序的所有特定形式的特定代码以及包括CTLAFTERUPDATEMYFORM程序。当然,如果没有特定的代码运行,则可能不存在ctlafterupdatemyform。这就是为什么我们将“ on Orror”转换为“ remume next” ...

选择这样一个通用的解决方案意义重大。这意味着您正在达到高水平的代码规范化(意味着无痛的代码维护)。而当你说你没有任何特定于表单的代码时,这也意味着表单模块已经完全标准化,并且它们的生产可以是 自动化的: :只需说出您想要在表单/控件级别管理哪些事件,并定义您的通用/特定过程术语。
一次性编写您的自动化代码。
这需要几天的工作,但会带来令人兴奋的结果。过去两年我一直在使用这个解决方案,它显然是正确的:我的表单是通过“表单表”从头开始完全自动创建的,链接到“控制表”。
然后我可以花时间处理表格的具体程序(如果有)。

即使使用 MS Access,代码规范化也是一个漫长的过程。但这种痛苦真的值得!

另一个优点是 Access 是一个 COM 应用程序 是你可以创建一个 .NET 应用程序通过自动化运行和测试 Access 应用程序. 。这样做的好处是你可以使用更强大的测试框架,例如 单位 针对 Access 应用程序编写自动断言测试。

因此,如果您精通 C# 或 VB.NET 并结合 NUnit 之类的东西,那么您可以更轻松地为 Access 应用程序创建更大的测试覆盖率。

尽管这是一个非常古老的答案:

账户单元, ,一个专门用于 Microsoft Access 的单元测试框架。

我已经取出一页了 Python 的文档测试 概念并在 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 要知道
  • 它只能用于返回适合单行的结果的函数

尽管有其局限性,但我仍然认为它物有所值。

编辑: :这是一个简单的函数,该函数必须满足“文档测试规则”。

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

我会设计应用程序,在查询和 vba 子例程中完成尽可能多的工作,以便您的测试可以由填充测试数据库、针对这些数据库运行生产查询和 vba 集,然后查看输出和进行比较以确保输出良好。这种方法显然不会测试 GUI,因此您可以通过手动执行的一系列测试脚本(这里我的意思是像一个 Word 文档,显示打开表单 1,然后单击控件 1)来增强测试。

这取决于项目的范围作为测试方面所需的自动化水平。

如果您有兴趣在更精细的级别(特别是 VBA 代码本身)测试您的 Access 应用程序,那么 VB精简版单元 是用于此目的的一个很棒的单元测试框架。

我发现我的应用程序中进行单元测试的机会相对较少。我编写的大部分代码都与表数据或文件系统交互,因此从根本上来说很难进行单元测试。早期,我尝试了一种可能类似于模拟(欺骗)的方法,我创建了具有可选参数的代码。如果使用了该参数,则该过程将使用该参数而不是从数据库中获取数据。设置与一行数据具有相同字段类型的用户定义类型并将其传递给函数非常容易。我现在有一种方法可以将测试数据输入到我想要测试的过程中。每个过程中都有一些代码将真实数据源替换为测试数据源。这使我能够使用我自己的单元测试功能对更广泛的功能进行单元测试。编写单元测试很容易,只是重复且无聊。最后,我放弃了单元测试并开始使用不同的方法。

我主要为自己编写内部应用程序,这样我就可以等到问题找到我,而不是必须拥有完美的代码。如果我确实为客户编写应用程序,通常客户并不完全了解软件开发成本是多少,因此我需要一种低成本的方式来获得结果。编写单元测试就是编写一个测试,将坏数据推送到过程中,以查看该过程是否可以正确处理它。单元测试还确认良好的数据得到了适当的处理。我当前的方法是将输入验证写入应用程序中的每个过程,并在代码成功完成时引发成功标志。每个调用过程在使用结果之前都会检查成功标志。如果出现问题,则会通过错误消息的方式进行报告。每个函数都有一个成功标志、一个返回值、一条错误消息、一条注释和一个来源。用户定义的类型(fr 表示函数返回)包含数据成员。任何给定的函数可能仅填充用户定义类型中的某些数据成员。当一个函数运行时,它通常返回 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 的文章 这与我上面写的很接近。本文对异常处理和错误处理进行了比较和对比。我上面的建议类似于异常处理。

我没有尝试过这个,但你可以尝试 将您的访问表单作为数据访问网页发布到 sharepoint 等 或者 就像网页一样 然后使用诸如 通过一系列测试来驱动浏览器。

显然,这不如直接通过单元测试驱动代码那么理想,但它可能会帮助您完成部分工作。祝你好运

Access 是一个 COM 应用程序。使用 COM,而不是 Windows API。在 Access 中进行测试。

Access 应用程序的最佳测试环境是 Access。您所有的表单/报告/表格/代码/查询都可用,有类似于MS Test的脚本语言(好吧,您可能不记得MS Test),有用于保存您的测试脚本和测试结果的数据库环境,您在这里学到的技能可以转移到您的应用程序中。

这里有很好的建议,但令我惊讶的是没有人提到集中式错误处理。您可以获得允许快速函数/子模板和添加行号的插件(我使用 MZ-tools)。然后将所有错误发送到一个函数,您可以在其中记录它们。然后,您还可以通过设置单个断点来中断所有错误。

数据访问页面已被 MS 弃用相当长一段时间,而且从一开始就从未真正发挥过作用(它们依赖于安装的 Office Widget,并且仅在 IE 中有效,而且当时效果很差)。

确实,可以获得焦点的访问控件只有在获得焦点时才具有窗口句柄(而那些无法获得焦点的控件,例如标签,则根本不具有窗口句柄)。这使得 Access 非常不适合窗口句柄驱动的测试机制。

事实上,我质疑为什么要在 Access 中进行此类测试。在我看来,这就像你的基本极限编程教条,并不是 XP 的所有原则和实践都可以适用于 Access 应用程序——方钉、圆孔。

因此,请退后一步,问问自己想要实现什么目标,并考虑一下您可能需要使用与基于在 Access 中无法工作的方法完全不同的方法。

或者这种自动化测试对于 Access 应用程序是否有效甚至有用。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top