缺少VBA中的单元测试。 (但是VBA缺少什么?)由于最近我对单元测试越来越感兴趣,因此我决定需要比Debug.Assert()更好的东西,因此我开始构建此框架。当前缺少大量功能,但是由于我是单元测试和接口的新手,所以我不想在意识到自己犯了巨大错误之前就不做深入的研究。代码很简单,但是效果很好。

我希望能够将输出运行到文件或直接窗口中,因此我创建了一个简单的IOutput接口,其中包含一个子例程。

IOutput.cls

Public Sub PrintLine(Optional ByVal object As Variant)
End Sub


以及一个实现它的Console类。 Console使用VBPredeclaredId = True创建默认实例。 Logger类暂时仍未实现。

Console.cls

Implements IOutput

Public Sub PrintLine(Optional ByVal object As Variant)
    If IsMissing(object) Then
        'newline
        Debug.Print vbNullString
    Else
        Debug.Print object
    End If
End Sub

Private Sub IOutput_PrintLine(Optional ByVal object As Variant)
    PrintLine object
End Sub


然后,UnitTest类将一个IOutput对象放入并将其存储为属性。我需要输出流可用于本地项目,但我不想将其公开给引用它的外部项目,因此我在Friend范围内对其进行了声明(稍后会详细介绍)。

UnitTest.cls

Private Type TUnitTest
    Name As String
    OutStream As IOutput
    Assert As Assert
End Type

Private this As TUnitTest

Public Property Get Name() As String
    Name = this.Name
End Property

Friend Property Get OutStream() As IOutput
    Set OutStream = this.OutStream
End Property

Public Property Get Assert() As Assert
    Set Assert = this.Assert
End Property

Friend Sub Initialize(Name As String, out As IOutput)
    this.Name = Name
    Set this.OutStream = out
    Set this.Assert = New Assert
    Set this.Assert.Parent = Me
End Sub


UnitTest创建自己的Assert对象实例。我在这里真的很担心。我不喜欢我必须传递测试名称以及正在测试的实际条件。

Assert.cls

Private Const PASS As String = "Pass"
Private Const FAIL As String = "Fail"

Private Type TAssert
    Parent As UnitTest
End Type

Private this As TAssert

Public Static Property Get Parent() As UnitTest
    Set Parent = this.Parent
End Property

Public Static Property Set Parent(ByVal Value As UnitTest)
    Set this.Parent = Value
End Property

Public Sub IsTrue(testName As String, condition As Boolean, Optional message As String)

    Dim output As String
    output = IIf(condition, PASS, FAIL)

    Report testName, output, message

End Sub

Public Sub IsFalse(testName As String, condition As Boolean, Optional message As String)
    Dim output As String
    output = IIf(condition, FAIL, PASS)

    Report testName, output, message
End Sub

Private Sub Report(testName As String, output As String, message As String)

    output = this.Parent.Name & "." & testName & ": " & output

    If message <> vbNullString Then
        output = output & ": " & message
    End If

    this.Parent.OutStream.PrintLine output
End Sub


最后,我不想将所有这些类导入我正在研究的每个项目中。当我对VBAUnit项目进行更改时,保持它们全部同步将是一场噩梦。因此,我将其实例更改为“ PublicNotCreatable”。


如果instancing属性为PublicNotCreatable,则该类在同一项目中使用时行为正常,但是可以在其他项目中声明该类类型的变量。另一个项目不能创建该类的新实例,但是可以具有该类类型的变量。为了允许另一个项目使用该类的新实例,包含该类的项目必须提供一个全局作用域函数,该函数创建一个类的新实例并将其返回给调用者。例如,假设Project1包含一个名为Class1的类,其Instancing属性为PublicNotCreatable。还假设Project2引用了Project1。


Cpearson.com

所以我有一个名为Provider的常规* .bas模块,其中包含此单个函数。

Provider。 bas

Public Function New_UnitTest(Name As String, out As IOutput) As UnitTest
    Set New_UnitTest = New UnitTest
    New_UnitTest.Initialize Name, out
End Function


然后,从另一个项目中,我将VBAUnit添加到引用中。 (如果您没有打开它,则必须单击浏览并导航到实际文件。)我就是这样做的,并编写了一些本质上可以自我测试的测试。

这就是Friend示波器发挥作用的地方。 VBAUnit可以访问Initialize子例程和OutputStream属性,但是它们对任何外部项目都不可见。

AssertConditionTest

此代码是样板代码。我创建的每个新测试都需要这些行。另外,一旦实现了文件记录器,就需要在此处确定将结果输出到何处。我不喜欢样板,但是我想不出办法解决它。我愿意就此提出建议。

Private test As VBAUnit.UnitTest

Private Sub Class_Initialize()
    Set test = VBAUnit.New_UnitTest(TypeName(Me), VBAUnit.Console)
End Sub

Private Sub Class_Terminate()
    Set test = Nothing
End Sub


随后进行实际测试。

Public Sub RunAllTests()
    IsTrueShouldPass
    IsTrueShouldFail
    IsFalseShouldPass
    IsFalseShouldFail
End Sub

Public Sub IsTrueShouldPass()
    test.Assert.IsTrue "IsTrueShouldPass", True
End Sub

Public Sub IsTrueShouldFail()
    test.Assert.IsTrue "IsTrueShouldFail", False
End Sub

Public Sub IsFalseShouldPass()
    test.Assert.IsFalse "IsFalseShouldPass", False, "with a message."
End Sub

Public Sub IsFalseShouldFail()
    test.Assert.IsFalse "IsFalseShouldFail", True, "with a message."
End Sub


最后,在这个项目中,我们有一个常规的* bas。这只是我们用于运行我们感兴趣的测试的废弃代码。

Public Sub TestTheTests()
    Dim test As New AssertConditionTest
    test.RunAllTests
    test.IsFalseShouldPass
End Sub


总结:


我以一种智能的方式使用界面吗?
无论如何,是否有必要放弃AssertConditionTest中的样板代码?

如何避免在每个Assert语句中传递“测试名称”并仍然得到这样的结果?我的方法充其量像是一个肮脏的技巧。


AssertConditionTest.IsTrueShouldPass: Pass
AssertConditionTest.IsTrueShouldFail: Fail
AssertConditionTest.IsFalseShouldPass: Pass: with a message.
AssertConditionTest.IsFalseShouldFail: Fail: with a message.



这是一个愚蠢的决定,使Assert成为自己的班级并保留一个父母UnitTest属性?


#1 楼


IOutput类模块(接口)


查看接口的使用方式:


this.Parent.OutStream.PrintLine output



output显然是String,这很有意义。但是接口的签名并不能反映这一点,并且令人困惑:


Public Sub PrintLine(Optional ByVal object As Variant)



为什么参数是可选的?为什么是变体? ...为什么称为对象?我本来希望这样的:

Public Sub PrintLine(ByVal output As String)


这会引导我去实现:


控制台类模块


如果参数是String,并且不是可选参数,则PrintLine的实现会变得更简单一些:

Option Explicit 'always. even if you're not **yet** declaring anything.
Implements IOutput

Public Sub PrintLine(ByVal output As String)
    Debug.Print output
End Sub

Private Sub IOutput_PrintLine(ByVal output As String)
    PrintLine output
End Sub


它看来您的Console类打算像.net System.Console一样使用,是静态类。

IOutput接口实现的上下文中,该类为静态是没有意义的,其PrintLine方法也没有可选参数。但是,如果将测试结果封装在其自己的类中...

Option Explicit

Public Enum TestOutcome
    Inconclusive
    Failed
    Succeeded
End Enum

Private Type TTestResult
    outcome As TestOutcome
    output As String
End Type

Private this As TTestResult

Public Property Get TestOutcome() As TestOutcome
    TestOutcome = this.outcome
End Property

Friend Property Let TestOutcome(ByVal value As TestOutcome)
    this.outcome = value
End Property

Public Property Get TestOutput() As String
    TestOutput = this.output
End Property

Friend Property Let TestOutput(ByVal value As String)
    this.output = value
End Property

Public Function Create(ByVal outcome As TestOutcome, ByVal output As String)

    Dim result As New TestResult
    result.TestOutcome = outcome
    result.TestOutput = output

    Set Create = result

End Function


...然后我将IOutput重命名为ITestOutput,并像这样更改签名:

Public Sub WriteResult(ByVal result As TestResult)
End Sub


Console看起来不一样:

Option Explicit
Implements ITestOutput

Public Sub WriteResult(ByVal result As TestResult)
    Debug.Print result.TestOutput
End Sub

Private Sub ITestOutput_WriteResult(ByVal result As TestResult)
    WriteResult result
End Sub


这使WriteResult的意图比PrintLine,并不会阻止您实现WriteLine(String)方法并将Console保留为静态实用工具类,并且,您有一个关于测试结果的概念,这个结论可能是不确定的,失败的或成功的。



UnitTest类模块


真是太客气了,这是我第一次看到在VBA中有必要使用Friend关键字。这非常聪明,它启用了VBA否则无法实现的一些功能:



工厂:现在可以使用参数值创建和初始化类,就像用构造函数创建。

不变性:类只能公开getter,并且从客户端代码的角度来看是不变的。

令人印象深刻。我希望10年前我已经意识到VBAProjects可以互相引用!



提供程序代码模块


我不知道像这样。我本可以将其设置为“静态”类模块(具有默认实例),并命名为UnitTestFactory

我也不喜欢方法名称-再次,标识符中的下划线在VBA中令人困惑。如果该代码在名为UnitTestFactory的类中,则该方法的名称可能只是Create

我不喜欢您要分配结果,然后在该引用上调用一个方法-它看起来非常尴尬,使用result变量会更清楚,我将使IOutput实现/引用成为工厂类的属性,将其从方法的签名中删除:

Option Explicit
Private Type TUnitTestFactory
    TestOutput As IOutput
End Type

Private this As TUnitTestFactory

Public Property Get TestOutput() As IOutput
    Set TestOutput = this.TestOutput 
End Property

Public Property Set TestOutput(ByVal value As IOutput)
    Set this.TestOutput = value
End Property

Public Function Create(ByVal testName As String) As UnitTest
    Dim result As New UnitTest
    Set result.Initialize testName, TestOutput
    Set Create = result
End Function




声明类模块


我从未见过像这样使用Static关键字(无论如何在VBA中是静态属性?)和Parent财产使我认为课堂做的比应做的更多。我相信我上面建议的TestOutcome枚举和TestResult类在这里会有所帮助...但是我不认为Assert的工作是报告测试结果-通过将责任保持在测试级别,您可以删除需要将测试名称传递给Assert方法。

问题是,该怎么做?

我想我会公开一个事件:

Public Event AssertCompleted(ByVal result As TestResult)


这将使像IsTrue这样的方法看起来像这样:这-请注意,我想调用变量UnitTest,所以我将类重命名为assert,并且放弃了类型的默认实例/静态性:

Public Sub IsTrue(ByVal condition As Boolean, Optional message As String)

    Dim outcome As TestOutcome
    outcome = IIf(condition, TestOutcome.Succeeded, TestOutcome.Failed)

    result = TestResult.Create(outcome, message)
    RaiseEvent AssertCompleted(result)

End Sub


...宾果游戏!



该代码是样板代码。我创建的每个新测试都需要这些行。


不用担心样板代码。这是必需的,因为这是客户端代码为输出接口指定实现的唯一逻辑位置-测试可能想要输出到文本文件,另一个可能想要输出到即时窗格,另一个可能想要发送输出到模式窗体上的列表框...指定它确实是客户端代码的工作。

评论


\ $ \ begingroup \ $
使用带有默认实例的静态类是genius。我本来应该想到的,但是我猜我在抽MSDN的文档。使用事件报告断言已完成也很出色。很好的建议。您将断言和报告的关注点分离也完全正确。静态属性隐藏在其中,因为我正在使用“ Insert”工具.....从那里学到的教训。从现在开始,我将其键入。
\ $ \ endgroup \ $
–RubberDuck
2014年9月14日下午1:20

\ $ \ begingroup \ $
另外,IOutput具有可选的变体,因为如果我不带参数调用Console.PrintLine或Logger.PrintLine,则希望它打印空白行。由于对象将被强制转换为字符串并进行打印,因此需要多种形式。我一定是在Ruby中找到的。不知道我会改变那一部分。
\ $ \ endgroup \ $
–RubberDuck
2014年9月14日下午13:58

\ $ \ begingroup \ $
@RubberDuck再次思考,...是什么阻止了Assert.IsTrue作为函数并返回TestResult,而不是引发事件?
\ $ \ endgroup \ $
– Mathieu Guindon♦
2014-09-14 19:33

\ $ \ begingroup \ $
据我所知,这是行为。它不问这是否正确,而是断言。它需要采取行动。
\ $ \ endgroup \ $
–RubberDuck
2014年9月14日在21:12