0

这个问题的动机是获得一个具体的例子,说明修改文档时好的、可单元测试的代码是什么样的。作为背景,我很欣赏类非常适合定义和验证事物,例如:

类模块是否适合修改文档似乎“依赖”(请参见此处的 Mat 的 Mug 答案:在 VBA 中,是否应在类模块中避免修改文档的代码)而且我无法找到许多单元测试的示例用于修改文档的代码(也许有一个很好的理由?)

无论如何,基于我对事物的有限理解,我认为,对于“将格式化表格添加到文档”的单词 Add-In,类模块为将格式化表格添加到文档的合理方法提供了基础。 . (如果我错了,请告诉我)

虽然我标记了 VBA-Excel,但我真的对 MS Word 示例(非常缺乏)更感兴趣,所以通过一个相当微不足道的 MS Word 示例,假设我有将格式化表格添加到指定范围内的文档的代码。

出于示例的目的,我们假设:

  • 事件的基本顺序是:
    • 将默认表添加到文档
    • 随后根据 INI 文件对其进行格式化
  • 为所有表指定的格式为:
    • 表格边框线颜色
    • 表格第 1 行底纹颜色
  • INI 文件指定了几个表
    • tbl1-边框=wdRed
    • tbl2-边框=wdGreen
    • tbl1-Shading=wdRed
    • tbl1-Shading=wdGreen

所以我的下一个问题是:

  • 我应该计划几节课?
    • 1 用于添加和格式化表格
    • 1 用于读取 INI 文件数据
  • 每个类模块的结构是什么样的?
  • 我应该(我可以)对以下代码进行单元测试吗:
    • 修改文档(添加表格)?
    • 读取 INI 文件?

我不指望任何人提供实际的工作代码。但是伪代码、一般建议和一些具体的指针可能会非常感激。

注意:如果这个问题太宽泛,我很乐意分成单独的问题

4

1 回答 1

2

工作表(或 Word 文档)只不过是封装状态/数据的对象。

您可以特意使用您的代码将依赖的接口(例如IWorksheet,或IDocument)包装工作表/文档,但这将是一项巨大的努力,实际上几乎没有任何好处 - 单元测试将有使用该接口的“假”实现,该实现将负责存储测试数据/状态,以便您的测试可以断言您正在测试的代码按预期工作。完全矫枉过正。

相反,编写您的代码,以便给它一个Worksheet实例(即避免与ActiveWorkbook和/或工作ActiveSheet),并做它需要做的任何事情。划分职责,这样当你调用一个方法时,你就没有 20,000 件事情要断言,以确保你的代码完成它所写的事情——但这不应该是任何新的或不同于你已经在做的事情,对吧?

'@Description("Adds a table named [tableName] on [sheet]. Returns the created table.")
Public Function AddTable(ByVal sheet As Worksheet, ByVal tableName As String) As ListObject
    'TODO: implement
End Function

对这种方法的测试可能如下所示:

'@TestMethod
Public Sub AddsListObjectToSpecifiedWorksheet()

    'Arrange
    Dim sheet As Worksheet
    Set sheet = ThisWorkbook.Worksheets.Add

    Dim sut As MyAwesomeClass
    Set sut = New MyAwesomeClass

    Const tableName As String = "TestTable1"

    If sheet.ListObjects.Count <> 0 Then _
        Assert.Inconclusive "Sheet already has a table."

    'Act
    sut.AddTable sheet, tableName

    'Assert
    Assert.IsTrue sheet.ListObjects.Count = 1, "Table was not added."
    sheet.Delete

End Sub

sheet设置和清理代码可以移动到测试模块中的专用/TestInitialize方法TestCleanup,因为该测试模块中的每个测试方法都可能需要一个新的工作表来使用,因为您希望每个测试都是独立的而不是与其他测试共享任何状态。

将设置和清理代码提取到测试模块中的专用方法可以消除实际测试方法中的绒毛。毕竟,测试模块是一个标准模块,可以有自己的私有字段和模块级常量:

'@TestMethod
Public Sub ReturnsListObjectReference()

    'Arrange
    Dim sut As MyAwesomeClass
    Set sut = New MyAwesomeClass

    If testSheet.ListObjects.Count <> 0 Then _
        Assert.Inconclusive "Sheet already has a table."

    'Act
    Dim result As ListObject
    Set result = sut.AddTable(testSheet, tableName)

    'Assert
    Assert.IsNotNothing result, "Table was not returned."
    Assert.AreSame result, testSheets.ListObjects(1), "Wrong table was returned."

End Sub

因此,您继续编写测试,每个测试都验证一个特定的行为:

'@TestMethod
Public Sub TableNameIsAsSpecified()

    'Arrange
    Dim sut As MyAwesomeClass
    Set sut = New MyAwesomeClass

    If testSheet.ListObjects.Count <> 0 Then _
        Assert.Inconclusive "Sheet already has a table."

    'Act
    Dim result As ListObject
    Set result = sut.AddTable(testSheet, tableName)

    'Assert
    Assert.AreEqual tableName, result.Name, "Table name wasn't set."

End Sub

这样,当您、未来的您或继承您的代码的任何人查看您的测试套件时,他们将确切地知道您的代码该做什么,并且通过运行测试,他们会知道您的代码执行了预期的操作.

您是否希望在修改代码以使表格具有蓝色边框而不是绿色边框时中断测试,这完全取决于您和您的要求。

在您涉及 INI 文件的特定情况下,IMO 的“文件”部分是一个实现细节,您不希望单元测试依赖于网络上某处的某个文件。相反,您将拥有一个类或数据结构来保存配置键/值对;测试的“安排”部分将负责设置配置数据,当你“行动”时,你将配置传递给你的 SUT,然后断言结果状态与指定的配置匹配。

读/写实际 INI 文件的代码完全是另一个问题,它有自己的测试代码,这也可以避免碰到文件系统:你想测试你的代码,而不是Scripting Runtime是否完成它的FileSystemObject工作。

请注意,就测试而言,是否AddTable是实用程序标准程序模块的成员MyAwesomeClass或某个实用程序标准程序模块绝对没有区别;单元测试不会告诉您如何重新组合/抽象功能和组织代码。


最新版本的 Rubberduck(预发布 2.1.x 版本)包括一个“假冒/存根”框架的开始,可以设置为通过挂钩到 VBA 运行时本身来拦截许多特定的标准库调用。例如,您不希望单元测试弹出 a MsgBox,但如果您正在测试的方法需要一个,您可以MsgBox在测试运行时拦截调用(甚至设置其返回值,例如模拟用户点击[Yes] 或 [No] 或 [Cancel]),但这完全是另一个话题。

于 2017-08-07T14:41:40.807 回答