测试框架支持
简介
在测试框架中运行测试时,可以单独测量每个测试的覆盖率。为此,Coco 必须知道每个测试开始和结束的时间,以及关于测试的其他信息。通过在测试中添加一些代码,这成为可能。
有两种方法可以实现
- 使用Coco 库将数据写入
.csexe
文件。 - 直接写入
.csexe
文件。
下一章将描述在通用情况下使用这些方法的步骤。在之后的章节中,将为几个测试框架给出具体示例。
使用 Coco 库
大多数测试框架都提供一种方法,在每个测试之前运行一些代码,在每个测试之后也运行一些代码。它们通常也有一个方式来获取测试的名字。如果测试框架支持这一点,它也支持单独测量每个测试的代码覆盖率并将它们分别写入到.csexe文件中。
通常,测试框架还会提供一个方式来获取测试结果以及测试的其他信息。这些信息也可以写入到.csexe文件中。
这一切都是通过调用 Coco 库来完成的。该库有两个版本,一个是C/C++版本的,另一个是C#版本的,它们有相同的功能,但语法不同。关于代码中的差异,请参阅下面的示例。
以下库调用必须在每个测试之前执行
接下来的库调用用于设置稍后由__coveragescanner_save()
写入到.csexe文件中的信息。它们可以在测试之前或之后运行,顺序可以是任意的。
- 使用
__coveragescanner_testname()
设置测试名称。在大多数框架中,每个测试不仅有一个名称,还有一个属于它的测试套件的名称。Coco通过允许使用层次名称来支持这个功能:由斜杠分隔的由多个部分组成的字符串。CoverageBrowser可以将层次名称作为树形结构来显示。因此,当知道测试套件的名称时,可以创建
Suite/Test
形式的名称,并用作__coveragescanner_testname()
的参数。调用此函数是强制性的。
- 使用
__coveragescanner_teststate()
设置测试状态。此函数通常是在测试之后调用的;它设置将被写入 {.csexe} 文件中的测试结果。但它也可以在测试之前调用,例如将结果设置为"SKIPPED"
。调用此函数不是强制性的。
- 如有必要,写入HTML注释。可以将任意其他数据以文本形式写入到
.csexe
文件。这是通过使用函数__coveragescanner_clear_html_comment()
和__coveragescanner_add_html_comment()
来完成的。这些函数使用一个内部变量,该变量是由
__coveragescanner_add_html_comment()
添加文本的。文本被持久存储,因此需要在添加新信息之前清除。因此,创建HTML注释的方法是首先调用__coveragescanner_clear_html_comment()
,然后调用__coveragescanner_add_html_comment()
一次或多次。
测试之后,将数据写入并使用以下库调用进行一些清理。它们必须按此顺序运行。
- 保存测试数据。使用
__coveragescanner_save()
,将测试名称、测试结果、HTML注释(如果存在)以及所有覆盖率计数器的所有内容写入到.csexe
文件。之后,所有计数器都设置为零。 - 重置测试名称。当测试程序完成后,它将最后测试之后的覆盖率数据写入到
.csmes
文件。这些数据仍然绑定到最后使用的测试名称。因此,为防止此类错误,将当前测试的名称设置为一个空字符串,使用
__coveragescanner_testname("")
。
示例
本节是关于必须插入到测试框架代码中的代码形式的简要总结。
由于新代码引用仅存在于仪器化代码中的函数,因此它们必须由条件编译语句保护。存在一个符号__COVERAGESCANNER__
,它仅在代码被仪器化时存在,并且可用于此目的。
C++中的总体结构
以下代码通常是在每次测试之前运行
#ifdef __COVERAGESCANNER__ // Code to get TESTNAME __coveragescanner_clear(); __coveragescanner_testname(TESTNAME); #endif
以下代码是在每次测试之后运行
#ifdef __COVERAGESCANNER__ // Code to get TESTRESULT and COMMENT __coveragescanner_teststate(TESTRESULT); __coveragescanner_clear_html_comment(); // if needed __coveragescanner_add_html_comment(COMMENT); // if needed __coveragescanner_save(); __coveragescanner_testname(""); #endif
有关具体实现的详细说明,请参阅CppUnit部分。在那里,第一段代码的实体版本出现在startTest()
成员函数中,第二段代码(未使用HTML注释函数)出现在endTest()
中。
在C#中的通用结构
C#的代码在这个抽象层与C#的代码非常相似。唯一的区别是条件语句有另一种语法,并且Coco库函数有前缀,CoverageScanner
。
每个测试运行前执行的代码
#if __COVERAGESCANNER__ // Code to get TESTNAME CoverageScanner.__coveragescanner_clear(); CoverageScanner.__coveragescanner_testname(TESTNAME); #endif
每个测试运行后执行的代码
#if __COVERAGESCANNER__ // Code to get TESTRESULT and COMMENT CoverageScanner.__coveragescanner_teststate(TESTRESULT); CoverageScanner.__coveragescanner_clear_html_comment(); // if needed CoverageScanner.__coveragescanner_add_html_comment(COMMENT); // if needed CoverageScanner.__coveragescanner_testname(""); CoverageScanner.__coveragescanner_save(); #endif
直接将数据写入.csexe
文件
指定执行过的测试的第二个方法是直接将信息写入.csexe
文件。当每个测试都是一个单独的程序,并且由脚本运行测试时,这种方法特别有用。
.csexe
文件是一个文本文件;测试信息由附加到文件的额外文本行组成。
与之前一样,某些信息必须在测试前添加,某些信息必须在测试后添加。以下操作必须在每个测试前执行
- 为了设置测试名称,应追加如下形式的行
*<name of the test>
到
.csexe
文件。字符
*
必须是行的第一个字符。从它到最后一个字符之间的所有内容都被视为测试名称。注意:您可以在行开头使用段落符号(
§
)而不是*
。§
必须使用Latin-1编码,其数值为167。由于这可能导致国际化问题,我们强烈建议不要使用它。它仅为了与程序的老版本兼容。
在测试之后,可以执行以下操作
- 为了设置测试状态,应追加如下形式的行
!<status>
到
.csexe
文件。字符
!
必须是行的第一个字符。从它到最后一个字符之间的所有内容都被视为测试状态。状态可以是以下字符串之一
PASSED
:测试已成功执行。FAILED
:测试未成功通过。INCIDENT
:测试未能成功执行(类似于失败)。CHECK_MANUALLY
:无法确定测试是否成功执行。SKIPPED
:测试被跳过。
- 为了附加执行注释,在应用程序执行后插入HTML文件的正文内容。
完整的注释必须遵循HTML语法,但只有
<body>
标签的内容被使用。因此,一个最小的注释可能如下所示:<html><body>我的注释</body></html>
。可以有多个HTML注释,并且注释和状态声明可以按任何顺序出现,但它们都必须在测试执行后附加到
.csexe
文件中。
示例
以下批处理文件执行测试First Test
并将执行状态设置为CHECK_MANUALLY
。
echo *First Test >> myapp.csexe myapp echo "<HTML><BODY>Execution of myapp</BODY></HTML>" >> myapp.csexe echo !CHECK_MANUALLY >> myapp.csexe
CppUnit
CppUnit是C++的单元测试框架。此环境可以很容易地修改以获取每个单元测试的代码覆盖率。
以下代码是一个例子,说明如何做到这一点
#include <cppunit/TestListener.h> #include <cppunit/BriefTestProgressListener.h> #include <cppunit/CompilerOutputter.h> #include <cppunit/extensions/TestFactoryRegistry.h> #include <cppunit/TestResult.h> #include <cppunit/TestResultCollector.h> #include <cppunit/TestRunner.h> class CoverageScannerListener : public CppUnit::TestListener { public: CoverageScannerListener() {} void startTest( CppUnit::Test *test ) { m_testFailed = false; #ifdef __COVERAGESCANNER__ int pos; // Adjusting the name of the test to display the tests // in a tree view in CoverageBrowser std::string testname = "CppUnit/" + test->getName(); while ( ( pos = testname.find( "::", 0 ) ) != std::string::npos ) testname.replace( pos, 2, "/" ); // Reset the code coverage data to get only the code coverage // of the actual unit test. __coveragescanner_clear(); __coveragescanner_testname( testname.c_str() ) ; #endif } void addFailure( const CppUnit::TestFailure &failure ) { m_testFailed = true; } void endTest( CppUnit::Test *test ) { #ifdef __COVERAGESCANNER__ // Recording the execution state in the coverage report if ( m_testFailed ) __coveragescanner_teststate( "FAILED" ); else __coveragescanner_teststate( "PASSED" ); // Saving the code coverage report of the unit test __coveragescanner_save(); __coveragescanner_testname( "" ); #endif } private: bool m_testFailed; // Prevents the use of the copy constructor and operator. CoverageScannerListener( const CoverageScannerListener © ); void operator =( const CoverageScannerListener © ); }; int main( int argc, char* argv[] ) { #ifdef __COVERAGESCANNER__ __coveragescanner_install( argv[0] ); #endif // Create the event manager and test controller CPPUNIT_NS::TestResult controller; // Add a listener that colllects test result CPPUNIT_NS::TestResultCollector result; controller.addListener( &result ); // Add a listener that print dots as test run. CPPUNIT_NS::BriefTestProgressListener progress; controller.addListener( &progress ); // Add a listener that saves the code coverage information CoverageScannerListener coveragescannerlistener; controller.addListener( &coveragescannerlistener ); // Add the top suite to the test runner CPPUNIT_NS::TestRunner runner; runner.addTest( CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest() ); runner.run( controller ); return result.wasSuccessful() ? 0 : 1; }
在示例中,我们做以下操作
- 我们编写一个CppUnit侦听器类,该类记录每个单元测试完成后每个单元测试的代码覆盖率。
我们希望通过启用和禁用Coco来运行程序。因此,我们使用宏
__COVERAGESCANNER__
进行条件编译。该宏在由Coco处理的每个文件中定义,无需任何#include
。在侦听器类
CppUnitListener
中,我们使用以下成员函数startTest()
函数:在每个测试开始之前都会调用此函数。在其中,我们使用CppUnit提供的信息计算出测试名称,并将其传递给Coco库的
__coveragescanner_testname()
函数。这就是CoverageBrowser中的执行名称。我们调用
__coveragescanner_clear()
函数清空内部数据库,确保此测试之前执行过的代码覆盖率被忽略。addFailure()
函数:在测试失败后调用。它只是设置一个标志,供其他函数使用。endTest()
函数:在测试结束后调用。它使用
__coveragescanner_teststate()
记录执行状态(PASSED
或FAILED
),然后使用__coveragescanner_save()
保存自己的代码覆盖率报告。
- 在
main()
函数中调用__coveragescanner_install()
。 - 我们将监听器添加到CppUnit的测试管理器,即类
CPPUNIT_NS::TestResult
。在上述示例中,这是通过以下行完成的CoverageScannerListener coveragescannerlistener; controller.addListener( &coveragescannerlistener );
Qt Test
Qt Test是Qt的单元测试框架。它可以轻松地适应以获取每个单元测试的代码覆盖率
- 在
main()
函数中调用__coveragescanner_install()
。 - 编写一个名为
TestCoverageObject
的QObject
子类。它必须记录每个单元测试结束时的代码覆盖率。 - 不要从
QObject
继承,让所有测试用例从TestCoverageObject
继承。 TestCoverageObject
类提供自己的init()
和cleanup()
槽位,这些槽位使用CoverageScanner API保存代码覆盖率报告。如果这些槽位也声明在测试用例类中,则有必要将它们重命名为initTest()
和cleanupTest()
。- 启用代码覆盖率编译您的项目。
TestCoverageObject
头文件
#ifndef _TEST_COVERAGE_OBJECT_H #define _TEST_COVERAGE_OBJECT_H #include <QObject> class TestCoverageObject : public QObject { Q_OBJECT public: virtual void initTest() {} virtual void cleanupTest() {} protected slots: void init() ; void cleanup(); }; #endif
TestCoverageObject
源文件
#include "testcoverageobject.h" #include <QTest> #include <QMetaObject> #include <QString> void TestCoverageObject::init() { #ifdef __COVERAGESCANNER__ __coveragescanner_clear(); #endif initTest(); } void TestCoverageObject::cleanup() { cleanupTest(); #ifdef __COVERAGESCANNER__ QString test_name="unittest/"; test_name+=metaObject()->className(); test_name+="/"; test_name+=QTest::currentTestFunction(); __coveragescanner_testname(test_name.toLatin1()); if (QTest::currentTestFailed()) __coveragescanner_teststate("FAILED"); else __coveragescanner_teststate("PASSED") ; __coveragescanner_save(); __coveragescanner_testname(""); #endif }
GoogleTest
GoogleTest是用于C++的单元测试框架。此环境可以轻松地适应以获取每个单元测试的代码覆盖率
- 在
main()
函数中调用__coveragescanner_install()
。 - 编写一个
TestEventListener
类,用于在每次单元测试完成后记录代码覆盖率报告。在执行测试项(类成员startTest()
)之前,监听器应设置名称(使用__coveragescanner_testname()
)并清除仪器(使用__coveragescanner_clear()
),以确保仅获取相关测试的覆盖率数据。在执行测试项时,应保存仪器和执行状态(使用__coveragescanner_teststate()
和__coveragescanner_save()
)到类成员endTest()
。类CodeCoverageListener
提供了一个实现示例。 - 将此监听器添加到GoogleTest监听器的
Append
函数(函数::testing::UnitTest::GetInstance()->listeners().Append()
)。 - 使用CoverageScanner编译单元测试。
#include <gtest/gtest.h> #include <stdlib.h> class CodeCoverageListener : public ::testing::TestEventListener { public: // Fired before any test activity starts. virtual void OnTestProgramStart(const ::testing::UnitTest& unit_test) { } // Fired before each iteration of tests starts. There may be more than // one iteration if GTEST_FLAG(repeat) is set. iteration is the iteration // index, starting from 0. virtual void OnTestIterationStart(const ::testing::UnitTest& unit_test, int iteration) { } // Fired before environment set-up for each iteration of tests starts. virtual void OnEnvironmentsSetUpStart(const ::testing::UnitTest& unit_test) { } // Fired after environment set-up for each iteration of tests ends. virtual void OnEnvironmentsSetUpEnd(const ::testing::UnitTest& unit_test) { } // Fired before the test case starts. virtual void OnTestCaseStart(const ::testing::TestCase& test_case) { } // Fired before the test starts. virtual void OnTestStart(const ::testing::TestInfo& test_info) { #ifdef __COVERAGESCANNER__ __coveragescanner_clear(); std::string test_name= std::string(test_info.test_case_name()) + '/' + std::string(test_info.name()); __coveragescanner_testname(test_name.c_str()); #endif } // Fired after a failed assertion or a SUCCESS(). virtual void OnTestPartResult(const ::testing::TestPartResult& test_part_result) { } // Fired after the test ends. virtual void OnTestEnd(const ::testing::TestInfo& test_info) { #ifdef __COVERAGESCANNER__ __coveragescanner_teststate("UNKNOWN"); if (test_info.result()) { if (test_info.result()->Passed()) __coveragescanner_teststate("PASSED"); if (test_info.result()->Failed()) __coveragescanner_teststate("FAILED"); } __coveragescanner_save(); #endif } // Fired after the test case ends. virtual void OnTestCaseEnd(const ::testing::TestCase& test_case) { } // Fired before environment tear-down for each iteration of tests starts. virtual void OnEnvironmentsTearDownStart(const ::testing::UnitTest& unit_test) { } // Fired after environment tear-down for each iteration of tests ends. virtual void OnEnvironmentsTearDownEnd(const ::testing::UnitTest& unit_test) { } // Fired after each iteration of tests finishes. virtual void OnTestIterationEnd(const ::testing::UnitTest& unit_test, int iteration) { } // Fired after all test activities have ended. virtual void OnTestProgramEnd(const ::testing::UnitTest& unit_test) { } } ; int main(int argc, char **argv){ //initialize CoverageScanner library #ifdef __COVERAGESCANNER__ __coveragescanner_install(argv[0]); #endif ::testing::FLAGS_gtest_output = "xml"; ::testing::UnitTest::GetInstance()->listeners().Append(new CodeCoverageListener); ::testing::InitGoogleTest(&argc, argv); //init google test framework return RUN_ALL_TESTS(); //run all tests which are listed in this project }
CxxTest
CxxTest是用于C++的单元测试框架。此环境可以轻松地适应以获取每个单元测试的代码覆盖率
- 在
main()
函数中调用__coveragescanner_install()
。 - 通过子类化现有监听器创建一个CxxTest
TestListener
类CoverageScannerListener
。在下面的示例中,这是
ErrorPrinter
。它将在每次单元测试完成后记录代码覆盖率报告。为了确保我们只获取相关测试的覆盖率数据,监听器应该在执行测试项(类成员enterTest()
)之前,将名称设置为__coveragescanner_testname()
并清除__coveragescanner_clear()
。当执行测试项时,应该在成员函数
leaveTest()
中保存仪器和执行状态,使用__coveragescanner_teststate()
和__coveragescanner_save()
。最后,必须重新实现这个类中的所有测试失败成员,以记录测试失败。
- 在
main()
函数调用中,调用CoverageScannerListener
的run()
函数,而不是调用CxxTest::ErrorPrinter().run()
。 - 启用 CoverageScanner 编译单元测试。
例如
#include <cxxtest/TestRunner.h> #include <cxxtest/TestListener.h> #include <cxxtest/TestTracker.h> #include <cxxtest/ValueTraits.h> #include <cxxtest/ValueTraits.h> #include <cxxtest/ErrorPrinter.h> class CoverageScannerListener : public CxxTest::ErrorPrinter { public: CoverageScannerListener(std::ostream &o=std::cout, const char *preLine = ":", const char *postLine = "") : CxxTest::ErrorPrinter( o, preLine , postLine ) {} int run() { return CxxTest::ErrorPrinter::run(); } void enterTest( const CxxTest::TestDescription & desc) { test_passed=true; #ifdef __COVERAGESCANNER__ // Adjust the name of the test to display the tests // in a tree view in CoverageBrowser std::string testname="CxxTest/"; testname += desc.suiteName(); testname += "/"; testname += desc.testName(); // Reset the code coverage data to get only the code coverage // of the actual unit test. __coveragescanner_clear(); __coveragescanner_testname(testname.c_str()); #endif return CxxTest::ErrorPrinter::enterTest( desc ); } void leaveTest( const CxxTest::TestDescription & desc) { #ifdef __COVERAGESCANNER__ // Record the execution state in the coverage report if (test_passed) __coveragescanner_teststate("PASSED"); else __coveragescanner_teststate("FAILED"); // Save the code coverage report of the unit test __coveragescanner_save(); #endif return CxxTest::ErrorPrinter::leaveTest( desc ); } void failedTest(const char *file, int line, const char *expression) { // Only record that the test fails test_passed=false; return CxxTest::ErrorPrinter::failedTest( file, line, expression ); } void failedAssert(const char *file, int line, const char *expression) { // Only record that the test fails test_passed = false; return CxxTest::ErrorPrinter::failedAssert( file, line, expression ); } void failedAssertEquals(const char *file, int line, const char *xStr, const char *yStr, const char *x, const char *y) { // Only record that the test fails test_passed=false; return CxxTest::ErrorPrinter::failedAssertEquals( file, line, xStr, yStr, x, y ); } void failedAssertSameData(const char *file, int line, const char *xStr, const char *yStr, const char *sizeStr, const void *x, const void *y, unsigned size) { // Only record that the test fails test_passed=false; return CxxTest::ErrorPrinter::failedAssertSameData( file, line, xStr, yStr, sizeStr, x, y, size ); } void failedAssertDelta(const char *file, int line, const char *xStr, const char *yStr, const char *dStr, const char *x, const char *y, const char *d) { // Only record that the test fails test_passed=false; return CxxTest::ErrorPrinter::failedAssertDelta( file, line, xStr, yStr, dStr, x, y, d ); } void failedAssertDiffers(const char *file, int line, const char *xStr, const char *yStr, const char *value) { // Only record that the test fails test_passed=false; return CxxTest::ErrorPrinter::failedAssertDiffers(file, line, xStr, yStr, value ); } void failedAssertLessThan(const char *file, int line, const char *xStr, const char *yStr, const char *x, const char *y) { // Only record that the test fails test_passed=false; return CxxTest::ErrorPrinter::failedAssertLessThan(file, line, xStr, yStr, x, y ); } void failedAssertLessThanEquals(const char *file, int line, const char *xStr, const char *yStr, const char *x, const char *y) { // Only record that the test fails test_passed=false; return CxxTest::ErrorPrinter::failedAssertLessThanEquals( file, line, xStr, yStr, x, y ); } void failedAssertRelation(const char *file, int line, const char *relation, const char *xStr, const char *yStr, const char *x, const char *y) { // Only record that the test fails test_passed=false; return CxxTest::ErrorPrinter::failedAssertRelation( file, line, relation, xStr, yStr, x, y); } void failedAssertPredicate(const char *file, int line, const char *predicate, const char *xStr, const char *x ) { // Only record that the test fails test_passed=false; return CxxTest::ErrorPrinter::failedAssertPredicate( file, line, predicate, xStr, x); } void failedAssertThrows(const char *file, int line, const char *expression, const char *type, bool otherThrown) { // Only record that the test fails test_passed=false; return CxxTest::ErrorPrinter::failedAssertThrows( file, line, expression, type, otherThrown ); } void failedAssertThrowsNot(const char *file, int line, const char *expression) { // Only record that the test fails test_passed=false; return CxxTest::ErrorPrinter::failedAssertThrowsNot( file, line, expression ); } private: bool test_passed; }; int main() { #ifdef __COVERAGESCANNER__ __coveragescanner_install(argv[0]); #endif // Use "CoverageScannerListener().run()" instead of "CxxTest::ErrorPrinter().run()" return CoverageScannerListener().run(); }
boost::test
boost::test 是 C++ 的单元测试框架,它是 boost 库的一部分。为了将其适应以获取每个单元测试的代码覆盖率
- 实现一个
TestObserver
,它继承自boost::unit_test_framework::test_observer
并存储在BoostTestObserver.hpp
。 - 还实现(在同一个文件中)一个
boost::test::fixture
并在执行每个boost::test
的开始和结束时定义它,使用:BOOST_GLOBAL_FIXTURE(FixtureName)
。该文件看起来像这样#ifndef COVERAGE_TEST_OBSERVER_INCLUDED #define COVERAGE_TEST_OBSERVER_INCLUDED #include <boost/test/framework.hpp> #include <boost/test/tree/observer.hpp> #include <boost/test/included/unit_test.hpp> #ifdef __COVERAGESCANNER__ class BoostTestObserver : public boost::unit_test_framework::test_observer { public: BoostTestObserver(const char* moduleName) : test_observer() , m_testSuiteName(moduleName) , m_isAnyTestcaseOpen(false) {} ~BoostTestObserver() { if (m_isAnyTestcaseOpen) close_current_testcase(); } /////////////////////////////////////////////////////////////////////////////// // test suite related events: virtual int priority() { return 1; } virtual void test_finish() { if (m_isAnyTestcaseOpen) { close_current_testcase(); } } virtual void test_aborted() { if (m_isAnyTestcaseOpen) { close_current_testcase(); } } const char* get_testsuite_name() { return m_testSuiteName.data(); } // called in the very beginning of the test suite: virtual void test_start( boost::unit_test_framework::counter_t NumberOfTestCases) { m_currentTestNumber = -1; m_numberOfTestCases = NumberOfTestCases; __coveragescanner_install(m_testSuiteName.c_str()); } /////////////////////////////////////////////////////////////////////////////// // test case related events: // called before each testcase's start virtual void test_unit_start( boost::unit_test_framework::test_unit const &unit) { // skip any calls which may being triggered by the 'TestWrap' fixture if(unit.full_name().find('/') == -1) { return; } if (++m_currentTestNumber) close_current_testcase(); m_currentCaseHasFailed = false; __coveragescanner_clear(); m_currentTestName.assign( unit.full_name().c_str()); __coveragescanner_testname(m_currentTestName.c_str()); m_isAnyTestcaseOpen = true; } // should be called after each testcase has finished... // but which definitveley, for unknown reason is NOT virtual void test_unit_finish( boost::unit_test_framework::test_unit const &unit, unsigned number) { close_current_testcase(); } // workaround for missing 'test_unit_finish' event being not triggered: void close_current_testcase() { if (m_isAnyTestcaseOpen) { __coveragescanner_teststate(m_currentCaseHasFailed ? "FAILED" : "PASSED"); __coveragescanner_save(); m_currentTestName.append("_invalid"); m_isAnyTestcaseOpen = false; } } // called once on each FAILED or PASSED result virtual void assertion_result(boost::unit_test::assertion_result result) { if (result == boost::unit_test::AR_FAILED) m_currentCaseHasFailed = true; } protected: // Deprecated boost event: virtual void assertion_result(bool result) { if (!result) m_currentCaseHasFailed = true; } /////////////////////////////////////////////////////////////////////////////// // member variables: std::string m_testSuiteName; std::string m_currentTestName; int m_currentTestNumber; int m_numberOfTestCases; bool m_verboseLogging; bool m_currentCaseHasFailed; bool m_isAnyTestcaseOpen; }; ////////////////// // boost fixture: // will be called in the very beginning and in the end of executing test /////////////////////////////////////////////////////////////////////////////// struct TestWrap { BoostTestObserver observer; TestWrap() : BoostTestObserver( boost::unit_test::framework::master_test_suite().argv[0]) { boost::unit_test::framework::register_observer(observer); } ~TestWrap() { observer.close_current_testcase(); boost::unit_test::framework::deregister_observer(observer); } }; /////////////////////////////////////////////////////////////////////////////// BOOST_GLOBAL_FIXTURE(TestWrap); #endif #endif
- 如果它被任何现有的
boost::test
单元测试.cpp
文件包含(必须在强制性的BOOST_TEST_MODULE
定义之后完成),那么它将由boost::test
自动实例化和初始化。 - 然后,
boost::test
将在开始时触发一个__coveragescanner_install()
调用,在每个正在执行的测试案例部分触发__coveragescanner_clear()
和__coveragescanner_testname()
调用。它还触发每个测试案例结束时的__coveragescanner_teststate()
和__coveragescanner_save()
。 - 此外,这里有一个小示例,展示如何将其包含到您现有的
boost::test
模块中// define the name for yourBoost::Test module. // (*this must be done BEFORE including BoostTestObserver.hpp) #define BOOST_TEST_MODULE MyBoostTest #include <boost/test/framework.hpp> #include <boost/test/tree/observer.hpp> #include <boost/test/included/unit_test.hpp> // any of your already existing Boost::Test modules can be made // compatible to Coco by including "BoostTestObserver.hpp" // just after the module's BOOST_TEST_MODULE definition. #include "BoostTestObserver.hpp" // declare a boost test suite: // (when "BoostTestObserver.hpp" is included, this will // automatically instantiate a BoostTestObserver then.) BOOST_AUTO_TEST_SUITE(BOOST_TEST_MODULE) // declare first test case: BOOST_AUTO_TEST_CASE(testA) { // code for testcase A } //... declare any other testcases // last testcase: BOOST_AUTO_TEST_CASE(testX) {//... } // in the end of the test, close the suite's scope // (which at least also will unload our BoostTestObserver) BOOST_AUTO_TEST_SUITE_END()
- 启用 CoverageScanner 编译 boost 测试。
xUnit
当 Coco 与 xUnit 一起使用时,可以保存每个单独测试的覆盖率。为此,我们需要定义在每次测试之前和之后执行的操作集合和钩子。
测试本身不必经过仪器化,但被测试的代码必须使用 CoverageScanner 构建。需要对 xUnit 测试套件进行小范围调整。这些更改即使在测试库不经过仪器化的情况下也能工作,但在此情况下,不会生成代码覆盖率信息。
按以下步骤进行
- 将以下代码添加到您的 xUnit 测试库中,该代码提供了一组处理覆盖率信息并保存执行报告(
.csexe
文件)的函数using System; using System.Collections.Generic; using System.Text; using Xunit; using System.Reflection; using Xunit.Sdk; using System.Diagnostics; using Xunit.Abstractions; class Coverage { public enum State { PASSED, FAILED, CHECK_MANUALLY, UNKNOWN }; public static void CoverageCleanup() { AppDomain MyDomain = AppDomain.CurrentDomain; Assembly[] AssembliesLoaded = MyDomain.GetAssemblies(); foreach (Assembly a in AssembliesLoaded) { Type coco = a.GetType("CoverageScanner"); if (coco != null) { // clear all coverage information to only get the code coverage of the current executed unit test coco.InvokeMember("__coveragescanner_clear", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, null); coco.InvokeMember("__coveragescanner_clear_html_comment", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, null); } } } public static void CoverageTextLog(String text, bool bold, bool italic) { String style_begin = ""; String style_end = ""; if (italic) { style_begin += "<I>"; style_end += "</I>"; } if (bold) { style_begin += "<B>"; style_end += "</B>"; } String comment = "<HTML><BODY><TT>" + style_begin + text.Replace("&", "&") .Replace("<", "<") .Replace(">", ">") .Replace("\"", """) .Replace("'", "'") .Replace("\n", "<BR>") .Replace("\r", "") + style_end + "</TT></BODY></HTML>" ; CoverageHtmlLog(comment); } public static void CoverageHtmlLog(String comment) { AppDomain MyDomain = AppDomain.CurrentDomain; Assembly[] AssembliesLoaded = MyDomain.GetAssemblies(); foreach (Assembly a in AssembliesLoaded) { Type coco = a.GetType("CoverageScanner"); if (coco != null) { coco.InvokeMember("__coveragescanner_add_html_comment", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, new object[] { comment }); } } } public static void CoverageRecord(String csexe_filename, String testName, State result) { if (csexe_filename == "") return; AppDomain MyDomain = AppDomain.CurrentDomain; Assembly[] AssembliesLoaded = MyDomain.GetAssemblies(); foreach (Assembly a in AssembliesLoaded) { Type coco = a.GetType("CoverageScanner"); if (coco != null) { String name = testName; String state = ""; switch (result) { case State.FAILED: state = ("FAILED"); break; case State.CHECK_MANUALLY: state = ("CHECK_MANUALLY"); break; case State.PASSED: state = ("PASSED"); break; } name = name.Replace('.', '/'); // set the execution state: PASSES, FAILED or CHECK_MANUALLY coco.InvokeMember("__coveragescanner_teststate", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, new object[] { state }); if (name.Length > 0) // Test name: Namespace/Class/Testfunction coco.InvokeMember("__coveragescanner_testname", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, new object[] { name }); // File name is <classname>.csexe coco.InvokeMember("__coveragescanner_filename", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, new object[] { csexe_filename }); // saves the code coverage data coco.InvokeMember("__coveragescanner_save", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, null); // clear all coverage information to only get the code coverage of the current executed unit test coco.InvokeMember("__coveragescanner_clear", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, null); coco.InvokeMember("__coveragescanner_clear_html_comment", BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, null); } } } }
- 将以下代码添加到您的 xUnit 测试库中,其中包含一个定义了在执行每个单元测试后保存覆盖率的数据集的 xUnit 集合。如果代码中已经有集合,则需要按照此文件作为指导对这个现有集合进行修改
using System; using System.Collections.Generic; using System.Text; using Xunit; using System.Reflection; using Xunit.Sdk; using System.Diagnostics; using Xunit.Abstractions; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class SquishCocoHooks : BeforeAfterTestAttribute { public override void Before(MethodInfo methodUnderTest) { Coverage.CoverageCleanup(); } public override void After(MethodInfo methodUnderTest) { string base_name = "xUnit"; string method_name = methodUnderTest.Name; string class_name = methodUnderTest.ReflectedType.FullName; Coverage.CoverageRecord("coverage.csexe", base_name + "/" + class_name + "/" + method_name, Coverage.State.UNKNOWN); } } public class CustomFixture : IDisposable { public CustomFixture() { // Some initialization } public void Dispose() { // Some cleanup } } [CollectionDefinition("SquishCoco")] [SquishCocoHooks] public class SquishCocoDefinition : ICollectionFixture<CustomFixture> { }
- 通过在每个测试类的定义中添加
[Collection("SquishCoco")]
激活每个测试类的集合。例如[Collection("SquishCoco")] public class Class2Tests { [Fact()] public void dummyTest() { Assert.True(false, "Dummy test"); } }
测试运行完成后,将生成一个文件 coverage.csexe
。然后可以将它导入对应的覆盖率数据库(.csmes
文件)。使用 CoverageBrowser 后,可以分别分析每个测试的覆盖率。
NUnit
Coco 为 NUnit 2.4.4 及更高版本提供了一个示例插件。要安装它
- 使用提供的示例目录中的 Visual Studio 项目
NUnitSquishCoco.vsproj
构建NUnitSquishCoco.dll
。 - 将
NUnitSquishCoco.dll
复制到addins
文件夹中,该文件夹位于可以找到NUnit.exe
的bin
文件夹。 - 启动
NUnit.exe
并验证插件NUnit \COCO
是否已加载。
安装完成后,一旦 NUnit 测试驱动程序执行一个 C# 或 C++ 管理单元测试 test.dll
,如果使用 Coco 修正常规库并且有 test.dll
,它将自动生成代码覆盖率执行报告 test.dll.csexe
。代码覆盖率信息组织成一个包含每个单个单元测试的覆盖率与执行状态的树。然后,可以将执行报告导入应用程序的仪器数据库中,使用 CoverageBrowser 或 cmcsexeimport。
Catch2
Catch2 是一个 C++ 单元测试框架,可以方便地修改以获取每个单元测试节的代码覆盖率。
完整的代码示例
* #define CATCH_CONFIG_RUNNER #include "catch.hpp" #include <string> #include <deque> class CoverageScannerListener : public Catch::TestEventListenerBase { public: using TestEventListenerBase::TestEventListenerBase; virtual void sectionStarting( Catch::SectionInfo const& sectionInfo ) override { m_testNameHierarchy.push_back( sectionInfo.name ); #ifdef __COVERAGESCANNER__ // Adjusting the name of the test for the CoverageBrowser std::string testname = implodeTestNameHierarchy(); // Reset the code coverage data to get only the code coverage // of the actual unit test. __coveragescanner_clear(); __coveragescanner_testname( testname.c_str() ); #endif } virtual void sectionEnded( Catch::SectionStats const& sectionStats ) override { m_testNameHierarchy.pop_back(); #ifdef __COVERAGESCANNER__ // Recording the execution state in the coverage report if ( sectionStats.assertions.allPassed() ) __coveragescanner_teststate( "PASSED" ); else __coveragescanner_teststate( "FAILED" ); // Saving the code coverage report of the unit test __coveragescanner_save(); __coveragescanner_testname( "" ); #endif } private: std::deque<std::string> m_testNameHierarchy; /* Helper method which generates a hierarchical name string usable by the coveragebrowser. */ std::string implodeTestNameHierarchy() { std::string fullTestName ( "Catch2" ); std::deque<std::string>::const_iterator it = m_testNameHierarchy.cbegin(); while ( it != m_testNameHierarchy.cend() ) { fullTestName += "/" + *it; it++; } return fullTestName; } }; // Register the coveragescanner listener with Catch2 CATCH_REGISTER_LISTENER( CoverageScannerListener ) int main( int argc, char* argv[] ) { #ifdef __COVERAGESCANNER__ __coveragescanner_install( argv[0] ); #endif return Catch::Session().run( argc, argv ); }
为了适配 Catch2
- 在
main()
函数中调用__coveragescanner_install()
和Catch::Session.run()
。Catch2 提供了一个预定义的main()
,在包含 Catch2 标头之前,需要通过定义CATCH_CONFIG_RUNNER
来忽略它。#define CATCH_CONFIG_RUNNER #include "catch.hpp"
- 创建一个继承自
Catch::TestEventListenerBase
的 Catch2 监听器类,它在每个单元测试完成后记录该节的代码覆盖率。请注意,在 Catch2 中,测试用例也被视为节点,因此不需要特定监听测试用例事件。在创建的监听器类(
CoverageScannerListener
)中,使用以下成员函数sectionStarting()
:在每个测试用例或节开始之前调用此函数,使用 Catch2 提供的信息计算测试名称,并通过__coveragescanner_testname()
将它传递给 Coco 库。我们还调用函数
__coveragescanner_clear()
来清空内部数据库,以确保忽略在此测试之前执行过的代码的覆盖率。sectionEnded()
:在测试用例或节完成后调用此函数。它使用
__coveragescanner_teststate()
来记录执行状态("通过" 或 "失败"),然后通过__coveragescanner_save()
保存代码覆盖率报告。
- 通过添加以下 Catch2 宏添加此监听器
CATCH_REGISTER_LISTENER( CoverageScannerListener )
注意: 确保在仪器化过程中排除任何测试框架源代码(参见 超越最小化仪器化)。
Squish
一起运行 GUI 测试工具 Squish 和 Coco 以获取 Squish 测试套件的 C/C++ 覆盖率。然后可以进行更深入的分析,将每个测试用例(及其结果)与其各自的覆盖率信息关联起来。这尤其正确是因为 Coco 具有比较单个执行的功能,包括计算最佳顺序。
通用方法
下述方法是基于能够将名称、结果和自由形式的注释等信息应用于每个执行(存储在 .csexe
文件中)的可能性。参见 测试套件与 Coco。
作为一个例子,我们将使用 Squish 测试 Qt 的 addressbook
和 Unix 系统上的 JavaScript 测试脚本。
- 首先,我们将使用正在运行的 Squish 测试用例名称初始化执行数据。
function main() { var currentAUT = currentApplicationContext(); var execution = currentAUT.cwd + "\\" + currentAUT.name + ".exe.csexe" var testCase = squishinfo.testCase; var testExecutionName = testCase.substr(testCase.lastIndexOf('/') + 1); var file = File.open(execution, "a"); file.write("*" + testExecutionName + "\n"); file.close(); var ctx = startApplication("addressbook"); ...
- 在此处插入主测试脚本。
- 在主测试脚本之后,我们将为覆盖率工具记录测试结果。
... // wait until AUT shutdown while (ctx.isRunning) { snooze(1); // increase time if not enough to dump coverage data } // test result summary and status var positive = test.resultCount("passes"); var negative = test.resultCount("fails") + test.resultCount("errors") + test.resultCount("fatals"); var msg = "TEST RESULTS - Passed: " + positive + " | " + "Failed/Errored/Fatal: " + negative; var status = negative == 0 ? "PASSED" : "FAILED"; var file = File.open(execution, "a"); file.write("<html><body>" + msg + "</body></html>\n"); file.write("!" + status + "\n") file.close(); }
当你执行包含这些步骤的脚本时,Coco 执行报告将加载测试用例名称、状态和执行摘要以及执行细节和注释。
简化以供重用
- 在
Test Suite Resources
下创建一个名为squishCocoLogging.js
的文件,其中包含以下函数function getExecutionPath() { var currentAUT = currentApplicationContext(); var execution = currentAUT.cwd + "\\" + currentAUT.name + ".exe.csexe" return execution; } function logTestNameToCocoReport(currentTestCase, execution) { var testExecutionName = currentTestCase.substr(currentTestCase.lastIndexOf('\\') + 1); var file = File.open(execution, "a"); file.write("*" + testExecutionName + "\n"); file.close(); } function logTestResultsToCocoReport(testInfo, execution){ var currentAUT = currentApplicationContext(); // wait until AUT shuts down while (currentAUT.isRunning) snooze(5); // collect test result summary and status var positive = testInfo.resultCount("passes"); var negative = testInfo.resultCount("fails") + testInfo.resultCount("errors") + testInfo.resultCount("fatals"); var msg = "TEST RESULTS - Passed: " + positive + " | " + "Failed/Errored/Fatal: " + negative; var status = negative == 0 ? "PASSED" : "FAILED"; // output results and status to Coco execution report file var file = File.open(execution, "a"); file.write("<html><body>" + msg + "</body></html>\n"); file.write("!" + status + "\n") file.close(); }
此代码的 Python 版本如下
import re def getExecutionPath(): currentAUT = currentApplicationContext() execution = "%(currAUTPath)s\\%(currAUTName)s.exe.csexe" % {"currAUTPath" : currentAUT.cwd, "currAUTName" : currentAUT.name} return execution def logTestNameToCocoReport(currentTestCase, execution): testExecutionName = re.search(r'[^\\]\w*$', currentTestCase) testExecutionName = testExecutionName.group(0) file = open(execution, "a") file.write("*" + testExecutionName + "\n") file.close() def logTestResultsToCocoReport(testInfo, execution): currentAUT = currentApplicationContext() # wait until AUT shuts down while (currentAUT.isRunning): snooze(5) # collect test result summary and status positive = testInfo.resultCount("passes") negative = testInfo.resultCount("fails") + testInfo.resultCount("errors") + testInfo.resultCount("fatals") msg = "TEST RESULTS - Passed: %(positive)s | Failed/Errored/Fatal: %(negative)s" % {'positive': positive, 'negative': negative} if negative == 0: status = "PASSED" else: status = "FAILED" # output results and status to Coco execution report file file = open(execution, "a") file.write("<html><body>" + msg + "</body></html>\n") file.write("!" + status + "\n") file.close()
- 在主测试脚本中的
startApplication()
之后添加以下函数调用execution = getExecutionPath(); logTestNameToCocoReport(squishinfo.testCase, execution);
在 Python 中
execution = getExecutionPath() logTestNameToCocoReport(squishinfo.testCase, execution)
- 在您脚本结束时,在关闭 AUT(例如,通过单击 文件 > 退出)后,调用以下函数
logTestResultsToCocoReport(test, execution);
在 Python 中
logTestResultsToCocoReport(test, execution)
- 如果您的 AUT 突然关闭或脚本发生错误,添加
try
、catch
、finally
可确保您的结果 Still 输出到 Coco 报告文件。
您的主要测试脚本应类似于以下内容
source(findFile("scripts","squishCocoLogging.JS")) function main() { startApplication("addressbook"); execution = getExecutionPath(); logTestNameToCocoReport(squishinfo.testCase, execution); try { // body of script } catch(e) { test.fail('An unexpected error occurred', e.message) } finally { logTestResultsToCocoReport(test, execution) } }
Python 版本
source(findFile("scripts","squishCocoLogging.py")) def main(): startApplication("addressbook") execution = getExecutionPath() logTestNameToCocoReport(squishinfo.testCase, execution) try: try: # body of script except Exception as e: test.fail("test failed: ", e) finally: logTestResultsToCocoReport(test,execution)
Coco v7.2.0©2024 Qt 公司有限公司。
Qt 以及相应的商标是芬兰及/或全球其他国家的有效期内的 Qt 公司的商标。所有其他商标均为其各自所有者的财产。