使用 Visual Studio 宏自动运行 Boost.Test






4.50/5 (3投票s)
使用 Visual Studio 宏实现自动化,以运行使用 Boost.Test 开发的单元测试为例。
引言
程序员的一个美德是懒惰。
在本文中,我想分享我使用宏实现 Visual Studio 自动化的经验。宏可以使 Visual Studio 实现很多很棒的功能。在我日常工作中,我使用宏的一些示例:运行编辑器中光标处的单元测试(本文主题)、反复运行应用程序直到崩溃(调试启动时的崩溃)、快速更改当前项目的调试命令行、快速切换 Debug 和 Release、在解决方案中查找项目(对于大型解决方案,此类宏非常有帮助)。另外,很重要的一点是,任何宏都可以绑定一个快捷键。但我不想过多地谈论如何使用或编写宏,只做简要说明。我打算将重点放在自动化任务上——运行使用 Boost.Test 开发的单元测试及其使用宏的解决方案。
我想描述如何创建宏,以帮助运行当前打开的包含测试源文件的文档中由光标指向的测试用例或测试套件。
背景
在我的一 C++ 项目中,我使用了 Boost.Test 框架进行单元测试。然后我面临一个问题,我需要为运行新测试用例输入以下行:
unit_tests.exe --run_test=Hadwared/Gpu/BaseAlgorithms/SomeFunctionBehavior/shouldReturnFalseWhenParamIsZero
相当长的字符串,不是吗?于是我开始寻找让生活更轻松的方法。我看到了两个明显的解决方案:测试重构和运行自动化。
测试重构
一个简单的解决方案是重构测试。删除类别并使测试名称更短。但是类别为我们提供了运行单元测试的灵活性。例如,如果我想只执行 GPU 测试。而且我喜欢长名称,因为我不想在执行日志中看到类似这样的内容:
Running 1 test case...
./tests/unit_tests/some_class_tests.cpp(10): error in "test_1": check true == false failed
更重要的是,短名称会使维护测试变得非常困难。因为你需要通过代码来理解测试的功能。而一个很好的替代文档的方法就是使用长测试名称。所以对我来说,这不是一个好的方法。
运行自动化
下一个解决方案是自动运行所需的单元测试。就像 C# 中的单元测试一样。按一个快捷键,就会执行文本编辑器中光标指向的测试用例(或测试套件)。很酷,不是吗?我想尝试将此用于 C++ 和 Boost.Test 框架。
如何创建和运行简单的宏
为了开发宏,我使用 Visual Studio Macros IDE。要运行它,只需转到 **工具/宏/宏 IDE**。运行 IDE 后,打开 **MyMacros/Module1** 并开始编写新宏。当然,如果你懂 Visual Basic。我不懂。但这对我来说并不是什么大困难。
让我们从一个 Hello world! 示例开始
Sub MyFirstMarcos()
Dim Str As String = InputBox("Hello world!", "Hello", "Hi")
End Sub
就是这样。我们的第一个宏已经准备好了。现在我们需要运行它。你可以直接从宏 IDE **调试/启动** 来运行它。但这没有意思。宏可以绑定快捷键来运行。让我们这样做。在 **工具/选项** 中,打开 **环境/键盘**,然后在“显示包含的命令”字段中,开始输入我们的宏名称 MyFirstMacors
。之后,只需设置一个快捷键并按“分配”。现在,如果你按下此快捷键,屏幕上应该会出现一个 InputBox。
不难。但这为我们使用宏实现自动化提供了非常大的机会。
Boost.Test
在描述用于运行单元测试的宏之前,我想简要介绍一下 Boost.Test。首先,Boost.Tests 中的测试用例组织在一个测试套件的树状结构中。这意味着一个测试套件可以包含测试用例或另一个测试套件。测试源代码示例:
BOOST_AUTO_TEST_SUITE ( MainTestSuite )
BOOST_AUTO_TEST_SUITE ( ClassA_Behavior )
BOOST_AUTO_TEST_CASE ( checkIfTwoPlusTwoIsFour )
{
BOOST_CHECK(2 + 2 == 4);
}
BOOST_AUTO_TEST_CASE ( checkIfTwoMinusTwoIsZero )
{
BOOST_CHECK(2 - 2 == 0);
}
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_SUITE ( ClassB_Behavior )
BOOST_AUTO_TEST_CASE ( checkIfFalseIsntTrue )
{
BOOST_CHECK(false != true);
}
BOOST_AUTO_TEST_CASE ( checkMeaningOfLife )
{
BOOST_CHECK(false == true);
}
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_SUITE_END()
在此示例中,MainTestSuite
包含两个测试套件:ClassA_Behavior
和 ClassB_Behavior
,它们各自包含两个测试用例。
作为 Boost.Test 的输出,我们得到一个可以运行的应用程序。默认情况下,它会检查所有单元测试。要指定要执行的具体测试用例,我们必须使用命令行参数运行它,并提供测试的路径。对于前面的示例,如果我们想运行测试 checkMeaningOfLife
,我们需要指定以下命令行:--run_test=MainTestSuite/ClassB_Behavior/checkMeaningOfLife
。
用于运行单元测试的宏
要求
宏用于运行单元测试的用例
- 如果光标在测试用例的函数体或头中,则必须运行该测试用例。
- 如果光标在测试套件的头中,则必须运行整个测试套件。
我刚才说的是“运行测试用例/套件”。但是,要做到这一点,宏必须找到包含我们要运行的测试的源文件的项目。它必须构建该项目,设置正确的命令行,然后运行(带或不带调试器)。这也属于宏的要求一部分。
算法
宏的要求为我们提供了一个非常简单的算法:
- 获取包含单元测试源代码的文档。
- 解析此文档,为单元测试应用程序构建命令行字符串。
- 构建命令行字符串。
- 查找包含此文档的项目。
- 为该项目设置命令行字符串。
- 构建此项目。
- 运行此项目(带或不带调试器)。
为了解析算法,我使用正则表达式。它为我提供了检测测试用例和测试套件头部的灵活性。此外,它还可以捕获用例和套件的名称。我使用这些名称来构建命令行。解析算法的主要复杂之处在于构建从测试套件到查找测试用例的正确路径。如果我们看之前看到的例子,要构建到 checkMeaningOfLife
的路径(路径是 MainTestSuite/ClassB_Behavior/checkMeaningOfLife
),我们需要跳过 ClassA_Behavior
测试套件以及 ClassB_Behavior
测试套件中的 checkIfFalseIsntTrue
测试用例。为了做到这一点,我们计算带有测试套件结束标记(由 BOOST_AUTO_TEST_SUITE_END
标记)的行数,并忽略带有测试套件头部标记(由 BOOST_AUTO_TEST_SUITE
标记)的相应行。与其说一千句话,不如看看宏的源代码。
源代码
'Macros to parse current document and construct correct command line string for project.'
Private Sub SelectProjectAndFillParamsForBoostTest()
'Get active document with sources of unit tests.'
Dim ActiveDoc As Document = DTE.ActiveDocument
'Parse source file to build command line string.'
Dim TestSuite As String = ""
Dim TestCase As String = ""
'Get text area that under cursor in current document'
Dim selection As TextSelection = CType(ActiveDoc.Selection(), TextSelection)
'Create edit point in current document'
Dim editPoint As EditPoint = selection.TopPoint.CreateEditPoint()
'Remember line with cursor'
Dim lineOriginal As Integer = selection.TopPoint.Line
'Parse line by line until find test case name.
'Save this name. If not we will run hole test suite.'
Dim line As Integer = lineOriginal
While line <> 0
'Get text in current line. If you will use selection for this,
'that may course moving cursor in document.'
Dim text As String = editPoint.GetLines(line, line + 1)
'Check if it suite for regular expression.'
Dim match As System.Text.RegularExpressions.Match = _
System.Text.RegularExpressions.Regex.Match(text, _
".*BOOST_(AUTO|FIXTURE)_TEST_CASE[\s]*\((.*)\)")
'If it is get test case name and leave.'
If Not match Is System.Text.RegularExpressions.Match.Empty Then
Dim Temp As String = Split(match.Groups.Item(2).Value.Trim(), ",").GetValue(0)
TestCase = Temp.Trim()
Exit While
End If
line = line - 1
End While
'Parse line by line until find test suites names
'which belong previous founded test case. Save these names.'
line = lineOriginal
Dim endCount As Integer = 0
While line <> 0
'Get text in current line. Using selection
'for this may course moving cursor in document.'
Dim text As String = editPoint.GetLines(line, line + 1)
'Check if it suite for regular expression.'
Dim match As System.Text.RegularExpressions.Match = _
System.Text.RegularExpressions.Regex.Match(text, _
".*BOOST_(AUTO|FIXTURE)_TEST_SUITE[\s]*\((.*)\)")
'If it is, get test suite name.'
If Not match Is System.Text.RegularExpressions.Match.Empty Then
'Check if we found correct test suite in tests tree.'
If endCount = 0 Then
Dim Temp As String = _
Split(match.Groups.Item(2).Value.Trim(), ",").GetValue(0)
If TestSuite <> "" Then
TestSuite = Temp.Trim() + "/" + TestSuite
Else
TestSuite = Temp.Trim()
End If
Else
endCount = endCount - 1
End If
End If
'Check if it suite for regular expression that represent the end of test suite.'
Dim matchEnd As System.Text.RegularExpressions.Match = _
System.Text.RegularExpressions.Regex.Match(text, ".*BOOST_AUTO_TEST_SUITE_END.*")
If Not matchEnd Is System.Text.RegularExpressions.Match.Empty Then
endCount = endCount + 1
End If
line = line - 1
End While
'Find project that contain document with unit tests.'
Dim Proj As Project = ActiveDoc.ProjectItem.ContainingProject
'Get it active configuration.'
Dim config As Configuration = Proj.ConfigurationManager.ActiveConfiguration
'Retrieve command line for running this project in that configuration.'
Dim CmdLine As EnvDTE.Property = config.Properties.Item("CommandArguments")
'Build new command line, that will allow us to run our unit tests.'
If TestCase = "" And TestSuite = "" Then
CmdLine.Value = ""
ElseIf TestCase = "" And TestSuite <> "" Then
CmdLine.Value = "--run_test=" & TestSuite
ElseIf TestCase <> "" And TestSuite = "" Then
CmdLine.Value = "--run_test=" & TestCase
ElseIf TestCase <> "" And TestSuite <> "" Then
CmdLine.Value = "--run_test=" & TestSuite & "/" & TestCase
End If
'Add some additional parameters if we need.'
CmdLine.Value = CmdLine.Value & " --log_level=test_suite"
'Set current project as startup project.'
Dim SoluBuild As SolutionBuild = DTE.Solution.SolutionBuild
Dim StartupProject As String
StartupProject = Proj.UniqueName
SoluBuild.StartupProjects = StartupProject
'Build current project.'
SoluBuild.BuildProject(config.ConfigurationName, Proj.UniqueName, True)
End Sub
'Macros to run project with unit tests.'
Sub RunCurrentBoostTest()
SelectProjectAndFillParamsForBoostTest()
'Start it without debugger.'
DTE.ExecuteCommand("Debug.StartWithoutDebugging")
End Sub
'Macros to run project with unit tests.'
Sub RunCurrentBoostTestDebug()
SelectProjectAndFillParamsForBoostTest()
'Start it with debugger.'
DTE.Debugger.Go()
End Sub
用法
要安装此宏,只需将其复制到包含其他宏的模块中,然后将某个快捷键分配给 RunCurrentBoostTest
和 RunCurrentBoostTestDebug
。例如,Ctrl + B + T 和 Ctrl + D + T。要使用,只需打开包含单元测试源代码的文档,并将光标放在测试用例的任何位置,然后按快捷键。
关注点
我想分享一个观察。有时很难理解如何使用宏执行某个操作。我认为这是由于文档不完善以及表示 Visual Studio 实体的对象的抽象级别很高。但不要放弃!几乎所有问题都可以解决。
关于 Boost.Test 框架的一点说明。它在注册和执行单元测试方面具有极大的灵活性。但在本文中,我只关注了开发一个通用简单解决方案的基本方面。