测试MS Access应用程序的最佳方法?

时间:2008-09-06 11:58:30

标签: database unit-testing ms-access vba

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

测试表单的一个主要问题是,只有少数控件具有hwnd句柄,而其他控件只能获得一个焦点,这使得自动化非常不透明,因为您无法获得控件列表形成行动。

有分享的经验吗?

12 个答案:

答案 0 :(得分:19)

1。编写可测试代码

首先,停止将业务逻辑写入您的Form的代码中。那不是它的地方。它无法在那里进行适当的测试。事实上,你真的不应该自己测试你的表单。它应该是一个简单的简单视图,响应用户交互,然后将响应这些操作的责任委托给另一个 可测试的类。

你是怎么做到的?熟悉Model-View-Controller pattern是一个好的开始。

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

那么,这很好,所有 但它与测试有什么关系?! 朋友,它有 一切 < / strong>与测试有关。我们所做的是使我们的代码可测试。在我提供的示例中,甚至没有理由尝试测试GUI。我们真正需要测试的唯一事情是model。这就是所有真实逻辑的所在。

所以,继续第二步。

2。选择单元测试框架

这里没有很多选择。大多数框架都需要安装COM加载项,大量的样板,奇怪的语法,编写测试作为评论等等。这就是我参与building one myself的原因,所以这部分答案不公正,但我'我会尝试对可用的内容进行公平的总结。

  1. AccUnit

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

  3. xlUnit xlUnit并不糟糕,但它也不好。它很笨重,并且有很多锅炉板代码。这是最糟糕的,但它在Access中不起作用。那就是了。

  4. 构建自己的框架

    been there and done that。它可能比大多数人想要的更多,但完全有可能在Native VBA代码中构建一个单元测试框架。

  5. Rubberduck VBE Add-In's Unit Testing Framework
    免责声明:我是共同开发者之一

    我有偏见,但这是我最喜欢的一群。

    • 很少甚至没有锅炉板代码。
    • Intellisense可用。
    • 项目已激活。
    • 比大多数这些项目更多的文档。
    • 它适用于大多数主要的办公应用程序,而不仅仅是Access。
    • 不幸的是,它是一个COM加载项,因此必须安装到您的计算机上。
  6. 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. 使用描述测试测试内容的测试名称。如果它变长,不要担心。最重要的是它具有描述性。

    5. 我知道答案有点长,而且很晚,但希望它可以帮助一些人开始为他们的VBA代码编写单元测试。

答案 1 :(得分:17)

我很欣赏诺克斯和大卫的答案。我的答案将介于他们之间:只需制作 不需要调试的表单

我认为表单应该专门用作它们的基本内容,意味着图形界面 ,这意味着它们不必调试!然后,调试作业仅限于您的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  如果没有要运行的特定代码,则可能不存在。这就是为什么我们转向  “错误”到“继续下一步”......

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

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

答案 2 :(得分:5)

Access being a COM application的另一个优点是您可以创建.NET application to run and test an Access application via Automation。这样做的好处是,您可以使用更强大的测试框架(如NUnit)来编写针对Access应用程序的自动断言测试。

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

答案 3 :(得分:5)

我从Python's 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次调用
  • 有一些idiosyncrasies associated with 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

答案 4 :(得分:5)

虽然这是一个非常古老的答案:

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

答案 5 :(得分:4)

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

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

答案 6 :(得分:2)

如果您有兴趣在更细粒度级别测试Access应用程序,特别是VBA代码本身,那么VB Lite Unit是一个很好的单元测试框架。

答案 7 :(得分:2)

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

我主要为自己编写内部应用程序,因此我可以等到问题找到我而不是必须拥有完美的代码。如果我为客户编写应用程序,通常客户并不完全了解软件开发成本,因此我需要一种低成本的方法来获得结果。编写单元测试就是编写一个测试,在一个过程中推送坏数据,以查看过程是否可以正确处理它。单元测试还确认正确处理好的数据。我当前的方法是基于将输入验证写入应用程序中的每个过程,并在代码成功完成时引发成功标志。在使用结果之前,每个调用过程都会检查成功标志。如果出现问题,则通过错误消息报告。每个函数都有一个成功标志,一个返回值,一个错误消息,一个注释和一个原点。用户定义的类型(函数返回的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不是。

写这篇文章几天后,我发现article on The CodeProject接近我上面写的内容。本文比较和对比了异常处理和错误处理。我上面提到的建议类似于异常处理。

答案 8 :(得分:1)

我没有尝试过此操作,但您可以尝试publish your access forms as data access web pages to something like sharepointjust as web pages,然后使用selenium等工具通过一系列测试来驱动浏览器。

显然,这不像通过单元测试直接驱动代码那样理想,但它可能会让你成为一部分。祝你好运

答案 9 :(得分:1)

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

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

答案 10 :(得分:1)

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

答案 11 :(得分:-1)

数据访问页已被MS弃用了相当长的一段时间,并且从未真正起作用(它们依赖于正在安装的Office Widgets,并且仅在IE中工作,而且只是非常糟糕)。

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

确实,我怀疑你为什么要在Access中进行这种测试。这听起来像是你的基本极限编程教条,并不是XP的所有原理和实践都可以适用于Access应用程序 - 方形钉,圆孔。

所以,退后一步,问自己你想要完成什么,并认为你可能需要使用完全不同的方法,而不是基于在Access中无法使用的方法。

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