首页 / 知识

关于数据库:测试MS Access应用程序的最佳方法?

2023-04-14 10:08:00

Best way to test a MS Access application?

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

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

有经验可以分享吗?


1.编写可测试的代码

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

你是怎样做的?熟悉Model-View-Controller模式是一个好的开始。

Model View Controller diagram

由于无法获得事件或接口,而不能同时获得事件或接口,因此在VBA中无法完美完成,但是您可以做到非常接近。考虑具有文本框和按钮的这种简单形式。

simple form with text box and button

在后面的表单代码中,我们将把TextBox的值包装在一个公共属性中,然后重新引发我们感兴趣的任何事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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的新类模块。这就是我们要测试的代码。请注意,它自然具有与我们的观点相似的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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

最后,我们的控制器将它们连接在一起。控制器侦听表单事件,并将更改传达给模型,并触发模型的例程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

现在,可以从任何其他模块运行此代码。出于本示例的目的,我使用了一个标准模块。我强烈建议您使用我提供的代码自行构建并查看其功能。

1
2
3
4
5
6
Private controller As FormController

Public Sub Run()
    Set controller = New FormController
    controller.Run
End Sub

因此,这很棒,但与测试有什么关系呢!朋友,它与测试有关。我们要做的是使我们的代码可测试。在我提供的示例中,没有任何理由可以尝试测试GUI。我们真正需要测试的唯一一件事是model。这就是所有真正的逻辑所在。

因此,继续第二步。

2.选择一个单元测试框架

这里没有很多选择。大多数框架都需要安装COM加载项,大量样板程序,怪异的语法,将测试编写为注释等。这就是为什么我自己参与构建自己的原因,因此我的答案这一部分并不公正,但我会尽力对所提供的内容进行公平的总结。

  • AccUnit

  • 仅在Access中有效。
  • 要求您将测试作为注释和代码的混合体编写。 (注释部分没有智能感知。
  • 有一个图形界面可以帮助您编写那些看起来很奇怪的测试。
  • 自2013年以来,该项目未见任何更新。
  • VB Lite单元
    我不能说我个人使用过它。它在那里,但是自2005年以来没有看到过更新。

  • xlUnit
    xlUnit并不糟糕,但也不是很好。这很笨拙,并且有很多样板代码。它是最坏的一个,但是在Access中不起作用。所以,那出来了。

  • 建立自己的框架

    我去过那里并且做到了。它可能比大多数人想了解的要多,但完全有可能在本机VBA代码中构建单元测试框架。

  • Rubberduck VBE外接程序的单元测试框架
    免责声明:我是合作开发人员之一。

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

  • 很少甚至没有样板代码。
  • Intellisense可用。
  • 该项目处于活动状态。
  • 比大多数这些项目更多的文档。
  • 它适用于大多数主要的办公应用程序,而不仅仅是Access。
  • 不幸的是,它是一个COM加载项,因此必须将其安装到您的计算机上。
  • 3.开始编写测试

    因此,回到第1节的代码。我们真正需要测试的唯一代码是MyModel.Reversed()函数。因此,让我们看一下该测试的外观。 (给出的示例使用Rubberduck,但这是一个简单的测试,可以转换为您选择的框架。)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    '@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事件!只需将标准代码放入事件中,如下所示:

    1
    2
    3
    4
    5
    6
    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,代码规范化也是一个漫长的过程。但这确实值得付出痛苦!


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

    因此,如果您精通C#或VB.NET以及NUnit之类的工具,则可以更轻松地为Access应用程序创建更大的测试范围。


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

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


    我已经从Python的doctest概念中抽出了一页,并在Access VBA中实现了DocTests过程。这显然不是成熟的单元测试解决方案。它仍然还很年轻,所以我怀疑我已经解决了所有错误,但是我认为它已经成熟到可以放归野外了。

    只需将以下代码复制到标准代码模块中,然后在Sub中按F5即可查看其运行情况:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    '>>> 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的模块复制,粘贴和运行上面的代码将产生:

    1
    2
    Module: 3 - 1 evaluates to:  2  Expected:  0
    Tests passed:  1  of  2

    快速注意事项:

    • 它没有依赖性(在Access中使用时)
    • 它使用Eval,它是Access.Application对象模型中的一个函数;这意味着您可以在Access之外使用它,但需要创建一个Access.Application对象并完全限定Eval调用
    • 有一些与Eval相关的特质要注意
    • 它只能用于返回适合单行的结果的函数

    尽管有其局限性,但我仍然认为它为您带来了很多好处。

    编辑:这是一个简单的函数,必须满足" doctest规则"。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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,因此您可以使用一系列手动执行的测试脚本(这里的意思是像打开文档1的Word文档,然后单击控件1)来扩大测试范围。

    它取决于项目范围以及测试方面所需的自动化程度。


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

    我主要为自己编写内部应用程序,因此我可以负担得起直到问题找到我的时间,而不必拥有完美的代码。如果我确实为客户编写应用程序,那么通常客户不会完全知道多少软件开发成本,因此我需要一种低成本的方法来获得结果。编写单元测试就是编写一个在程序中推送不良数据的测试,以查看该程序是否可以正确处理它。单元测试还确认正确处理了良好的数据。我当前的方法是基于将输入验证写入应用程序中的每个过程,并在代码成功完成后引发成功标志。每个调用过程都在使用结果之前检查成功标志。如果发生问题,将通过错误消息进行报告。每个函数都有一个成功标志,一个返回值,一个错误消息,一个注释和一个原点。用户定义的类型(函数返回为fr)包含数据成员。许多给定的函数只能填充用户定义类型中的某些数据成员。运行函数时,通常返回success = true,并返回值,有时还返回注释。如果函数失败,则返回success = false和错误消息。如果功能链失败,则错误消息将以菊花形式更改,但结果实际上比普通堆栈跟踪更具可读性。起源也被链接在一起,所以我知道问题出在哪里。该应用程序很少崩溃,并且可以准确报告任何问题。结果比标准错误处理要好得多。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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是用户定义的枚举,如下所示

    1
    2
    3
    4
    5
    Public Enum eOutputFolder
        eDefaultDirectory = 1
        eAppPath = 2
        eCustomPath = 3
    End Enum

    我正在使用Enum将参数传递给函数,因为这会创建函数可以接受的一组有限的已知选择。在函数中输入参数时,枚举还提供智能感知。我想它们为功能提供了基本的接口。

    1
    2
    3
    4
    5
    6
    7
    8
    '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却不是。

    撰写本文后几天,我在The CodeProject上发现了一篇与我上面所写内容接近的文章。本文比较并对比了异常处理和错误处理。我上面的建议类似于异常处理。

    好。


    如果您有兴趣在更详细的级别(尤其是VBA代码)上测试Access应用程序,则VB Lite单元是用于此目的的出色单元测试框架。


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


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

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


    我还没有尝试过,但是您可以尝试将访问表单作为数据访问网页发布到诸如共享点之类的网页上,或仅作为网页发布,然后使用诸如selenium之类的工具通过一系列测试来驱动浏览器。

    显然,这不像直接通过单元测试来驱动代码那样理想,但是它可能会让您有所作为。祝好运


    MS已弃用Data Access Page已有相当长的一段时间了,但从一开始它就从来没有真正起作用过(它们依赖于所安装的Office Widget,并且只能在IE中工作,然后才很糟糕)。

    确实,可以获得焦点的Access控件只有在获得焦点时才具有窗口句柄(而那些不能获得焦点的控件(例如标签)根本就没有窗口句柄)。这使得Access特别不适用于窗口句柄驱动的测试方案。

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

    因此,退后一步,问自己要完成的工作,并考虑与基于Access中无法使用的方法的方法完全不同的方法。

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


    应用程序方法关于数据库表单

    最新内容

    相关内容

    猜你喜欢