繁体   English   中英

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

[英]Best way to test a MS Access application?

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

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

有经验可以分享吗?

1.编写可测试代码

首先,停止将业务逻辑写入您的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加载项,大量的样板,奇怪的语法,写测试作为评论等等。这就是为什么我自己参与构建一个 ,所以这部分答案不公正,但我会尝试对可用的内容进行公平的总结。

  1. AccUnit

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

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

  4. 建立自己的框架

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

  5. Rubberduck VBE加载项的单元测试框架
    免责声明:我是其中一个共同开发者

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

    • 很少甚至没有锅炉板代码。
    • Intellisense可用。
    • 该项目很活跃。
    • 比大多数这些项目更多的文档。
    • 它适用于大多数主要的办公应用程序,而不仅仅是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上更新控件时运行的特定过程

我有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应用程序创建更大的测试覆盖率。

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

AccUnit ,一个专门用于Microsoft 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

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

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

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

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

写完这篇文章几天后,我发现了一篇关于The CodeProject文章 ,它与我上面写的内容很接近。 本文比较和对比了异常处理和错误处理。 我上面提到的内容类似于异常处理。

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

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

我没有尝试过,但您可以尝试将访问表单作为数据访问网页发布到sharepointweb页面 ,然后使用selenium等工具通过一系列测试来驱动浏览器。

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

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

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM