测试框架支持

简介

在测试框架中运行测试时,可以单独测量每个测试的覆盖率。为此,Coco 必须知道每个测试开始和结束的时间,以及关于测试的其他信息。通过在测试中添加一些代码,这成为可能。

有两种方法可以实现

  1. 使用Coco 库将数据写入.csexe文件。
  2. 直接写入.csexe文件。

下一章将描述在通用情况下使用这些方法的步骤。在之后的章节中,将为几个测试框架给出具体示例。

使用 Coco 库

大多数测试框架都提供一种方法,在每个测试之前运行一些代码,在每个测试之后也运行一些代码。它们通常也有一个方式来获取测试的名字。如果测试框架支持这一点,它也支持单独测量每个测试的代码覆盖率并将它们分别写入到.csexe文件中。

通常,测试框架还会提供一个方式来获取测试结果以及测试的其他信息。这些信息也可以写入到.csexe文件中。

这一切都是通过调用 Coco 库来完成的。该库有两个版本,一个是C/C++版本的,另一个是C#版本的,它们有相同的功能,但语法不同。关于代码中的差异,请参阅下面的示例

以下库调用必须在每个测试之前执行

  1. 使用__coveragescanner_clear()清除计数器。测试开始时,此函数将 Coco 的所有内部覆盖率计数器设置为0。这可以防止被程序早期活动的数据所污染。

    函数描述:C/C++C#

接下来的库调用用于设置稍后由__coveragescanner_save()写入到.csexe文件中的信息。它们可以在测试之前或之后运行,顺序可以是任意的。

  1. 使用__coveragescanner_testname()设置测试名称。在大多数框架中,每个测试不仅有一个名称,还有一个属于它的测试套件的名称。Coco通过允许使用层次名称来支持这个功能:由斜杠分隔的由多个部分组成的字符串。CoverageBrowser可以将层次名称作为树形结构来显示。

    因此,当知道测试套件的名称时,可以创建Suite/Test形式的名称,并用作__coveragescanner_testname()的参数。

    调用此函数是强制性的。

    函数描述:C/C++C#

  2. 使用__coveragescanner_teststate()设置测试状态。此函数通常是在测试之后调用的;它设置将被写入 {.csexe} 文件中的测试结果。但它也可以在测试之前调用,例如将结果设置为"SKIPPED"

    调用此函数不是强制性的。

    函数描述:C/C++C#

  3. 如有必要,写入HTML注释。可以将任意其他数据以文本形式写入到.csexe文件。这是通过使用函数__coveragescanner_clear_html_comment()__coveragescanner_add_html_comment()来完成的。

    这些函数使用一个内部变量,该变量是由__coveragescanner_add_html_comment()添加文本的。文本被持久存储,因此需要在添加新信息之前清除。因此,创建HTML注释的方法是首先调用__coveragescanner_clear_html_comment(),然后调用__coveragescanner_add_html_comment()一次或多次。

    清除函数描述:C/C++C#
    添加函数描述:C/C++C#

测试之后,将数据写入并使用以下库调用进行一些清理。它们必须按此顺序运行。

  1. 保存测试数据。使用__coveragescanner_save(),将测试名称、测试结果、HTML注释(如果存在)以及所有覆盖率计数器的所有内容写入到.csexe文件。之后,所有计数器都设置为零。

    函数描述:C/C++C#

  2. 重置测试名称。当测试程序完成后,它将最后测试之后的覆盖率数据写入到.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文件是一个文本文件;测试信息由附加到文件的额外文本行组成。

与之前一样,某些信息必须在测试前添加,某些信息必须在测试后添加。以下操作必须在每个测试前执行

  1. 为了设置测试名称,应追加如下形式的行
    *<name of the test>

    .csexe文件。

    字符*必须是行的第一个字符。从它到最后一个字符之间的所有内容都被视为测试名称。

    注意:您可以在行开头使用段落符号(§)而不是*§必须使用Latin-1编码,其数值为167。由于这可能导致国际化问题,我们强烈建议不要使用它。它仅为了与程序的老版本兼容。

在测试之后,可以执行以下操作

  1. 为了设置测试状态,应追加如下形式的行
    !<status>

    .csexe文件。

    字符!必须是行的第一个字符。从它到最后一个字符之间的所有内容都被视为测试状态。

    状态可以是以下字符串之一

    • PASSED:测试已成功执行。
    • FAILED:测试未成功通过。
    • INCIDENT:测试未能成功执行(类似于失败)。
    • CHECK_MANUALLY:无法确定测试是否成功执行。
    • SKIPPED:测试被跳过。
  2. 为了附加执行注释,在应用程序执行后插入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 &copy );
        void operator =( const CoverageScannerListener &copy );
};

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;
}

在示例中,我们做以下操作

  1. 我们编写一个CppUnit侦听器类,该类记录每个单元测试完成后每个单元测试的代码覆盖率。

    我们希望通过启用和禁用Coco来运行程序。因此,我们使用宏__COVERAGESCANNER__进行条件编译。该宏在由Coco处理的每个文件中定义,无需任何#include

    在侦听器类CppUnitListener中,我们使用以下成员函数

    • startTest()函数:在每个测试开始之前都会调用此函数。

      在其中,我们使用CppUnit提供的信息计算出测试名称,并将其传递给Coco库的__coveragescanner_testname()函数。这就是CoverageBrowser中的执行名称。

      我们调用__coveragescanner_clear()函数清空内部数据库,确保此测试之前执行过的代码覆盖率被忽略。

    • addFailure()函数:在测试失败后调用。它只是设置一个标志,供其他函数使用。
    • endTest()函数:在测试结束后调用。

      它使用__coveragescanner_teststate()记录执行状态(PASSEDFAILED),然后使用__coveragescanner_save()保存自己的代码覆盖率报告。

  2. main()函数中调用__coveragescanner_install()
  3. 我们将监听器添加到CppUnit的测试管理器,即类CPPUNIT_NS::TestResult。在上述示例中,这是通过以下行完成的
    CoverageScannerListener coveragescannerlistener;
    controller.addListener( &coveragescannerlistener );

Qt Test

Qt Test是Qt的单元测试框架。它可以轻松地适应以获取每个单元测试的代码覆盖率

  1. main()函数中调用__coveragescanner_install()
  2. 编写一个名为TestCoverageObjectQObject子类。它必须记录每个单元测试结束时的代码覆盖率。
  3. 不要从QObject继承,让所有测试用例从TestCoverageObject继承。
  4. TestCoverageObject类提供自己的init()cleanup()槽位,这些槽位使用CoverageScanner API保存代码覆盖率报告。如果这些槽位也声明在测试用例类中,则有必要将它们重命名为initTest()cleanupTest()
  5. 启用代码覆盖率编译您的项目。

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++的单元测试框架。此环境可以轻松地适应以获取每个单元测试的代码覆盖率

  1. main()函数中调用__coveragescanner_install()
  2. 编写一个TestEventListener类,用于在每次单元测试完成后记录代码覆盖率报告。在执行测试项(类成员startTest())之前,监听器应设置名称(使用__coveragescanner_testname())并清除仪器(使用__coveragescanner_clear()),以确保仅获取相关测试的覆盖率数据。在执行测试项时,应保存仪器和执行状态(使用__coveragescanner_teststate()__coveragescanner_save())到类成员endTest()。类CodeCoverageListener提供了一个实现示例。
  3. 将此监听器添加到GoogleTest监听器的Append函数(函数::testing::UnitTest::GetInstance()->listeners().Append())。
  4. 使用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++的单元测试框架。此环境可以轻松地适应以获取每个单元测试的代码覆盖率

  1. main()函数中调用__coveragescanner_install()
  2. 通过子类化现有监听器创建一个CxxTest TestListenerCoverageScannerListener

    在下面的示例中,这是 ErrorPrinter。它将在每次单元测试完成后记录代码覆盖率报告。为了确保我们只获取相关测试的覆盖率数据,监听器应该在执行测试项(类成员 enterTest())之前,将名称设置为 __coveragescanner_testname() 并清除 __coveragescanner_clear()

    当执行测试项时,应该在成员函数 leaveTest() 中保存仪器和执行状态,使用 __coveragescanner_teststate()__coveragescanner_save()

    最后,必须重新实现这个类中的所有测试失败成员,以记录测试失败。

  3. main() 函数调用中,调用 CoverageScannerListenerrun() 函数,而不是调用 CxxTest::ErrorPrinter().run()
  4. 启用 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 库的一部分。为了将其适应以获取每个单元测试的代码覆盖率

  1. 实现一个 TestObserver,它继承自 boost::unit_test_framework::test_observer 并存储在 BoostTestObserver.hpp
  2. 还实现(在同一个文件中)一个 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
  3. 如果它被任何现有的 boost::test 单元测试 .cpp 文件包含(必须在强制性的 BOOST_TEST_MODULE 定义之后完成),那么它将由 boost::test 自动实例化和初始化。
  4. 然后,boost::test 将在开始时触发一个 __coveragescanner_install() 调用,在每个正在执行的测试案例部分触发 __coveragescanner_clear()__coveragescanner_testname() 调用。它还触发每个测试案例结束时的 __coveragescanner_teststate()__coveragescanner_save()
  5. 此外,这里有一个小示例,展示如何将其包含到您现有的 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()
  6. 启用 CoverageScanner 编译 boost 测试。

xUnit

当 Coco 与 xUnit 一起使用时,可以保存每个单独测试的覆盖率。为此,我们需要定义在每次测试之前和之后执行的操作集合和钩子。

测试本身不必经过仪器化,但被测试的代码必须使用 CoverageScanner 构建。需要对 xUnit 测试套件进行小范围调整。这些更改即使在测试库不经过仪器化的情况下也能工作,但在此情况下,不会生成代码覆盖率信息。

按以下步骤进行

  1. 将以下代码添加到您的 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("&", "&amp;")
                                    .Replace("<", "&lt;")
                                    .Replace(">", "&gt;")
                                    .Replace("\"", "&quot;")
                                    .Replace("'", "&apos;")
                                    .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);
                }
            }
        }
    }
  2. 将以下代码添加到您的 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>
    {
    }
  3. 通过在每个测试类的定义中添加 [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 及更高版本提供了一个示例插件。要安装它

  1. 使用提供的示例目录中的 Visual Studio 项目 NUnitSquishCoco.vsproj 构建 NUnitSquishCoco.dll
  2. NUnitSquishCoco.dll 复制到 addins 文件夹中,该文件夹位于可以找到 NUnit.exebin 文件夹。
  3. 启动 NUnit.exe 并验证插件 NUnit \COCO 是否已加载。

安装完成后,一旦 NUnit 测试驱动程序执行一个 C# 或 C++ 管理单元测试 test.dll,如果使用 Coco 修正常规库并且有 test.dll,它将自动生成代码覆盖率执行报告 test.dll.csexe。代码覆盖率信息组织成一个包含每个单个单元测试的覆盖率与执行状态的树。然后,可以将执行报告导入应用程序的仪器数据库中,使用 CoverageBrowsercmcsexeimport

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

  1. main() 函数中调用 __coveragescanner_install()Catch::Session.run()。Catch2 提供了一个预定义的 main(),在包含 Catch2 标头之前,需要通过定义 CATCH_CONFIG_RUNNER 来忽略它。
    #define CATCH_CONFIG_RUNNER
        #include "catch.hpp"
  2. 创建一个继承自 Catch::TestEventListenerBase 的 Catch2 监听器类,它在每个单元测试完成后记录该节的代码覆盖率。请注意,在 Catch2 中,测试用例也被视为节点,因此不需要特定监听测试用例事件。

    在创建的监听器类(CoverageScannerListener)中,使用以下成员函数

    • sectionStarting():在每个测试用例或节开始之前调用此函数,使用 Catch2 提供的信息计算测试名称,并通过 __coveragescanner_testname() 将它传递给 Coco 库。

      我们还调用函数 __coveragescanner_clear() 来清空内部数据库,以确保忽略在此测试之前执行过的代码的覆盖率。

    • sectionEnded():在测试用例或节完成后调用此函数。

      它使用 __coveragescanner_teststate() 来记录执行状态("通过" 或 "失败"),然后通过 __coveragescanner_save() 保存代码覆盖率报告。

  3. 通过添加以下 Catch2 宏添加此监听器
    CATCH_REGISTER_LISTENER( CoverageScannerListener )

注意: 确保在仪器化过程中排除任何测试框架源代码(参见 超越最小化仪器化)。

Squish

一起运行 GUI 测试工具 Squish 和 Coco 以获取 Squish 测试套件的 C/C++ 覆盖率。然后可以进行更深入的分析,将每个测试用例(及其结果)与其各自的覆盖率信息关联起来。这尤其正确是因为 Coco 具有比较单个执行的功能,包括计算最佳顺序。

通用方法

下述方法是基于能够将名称、结果和自由形式的注释等信息应用于每个执行(存储在 .csexe 文件中)的可能性。参见 测试套件与 Coco

作为一个例子,我们将使用 Squish 测试 Qt 的 addressbook 和 Unix 系统上的 JavaScript 测试脚本。

  1. 首先,我们将使用正在运行的 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");
    ...
  2. 在此处插入主测试脚本。
  3. 在主测试脚本之后,我们将为覆盖率工具记录测试结果。
    ...
        // 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 执行报告将加载测试用例名称、状态和执行摘要以及执行细节和注释。

简化以供重用

  1. 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()
  2. 在主测试脚本中的 startApplication() 之后添加以下函数调用
    execution = getExecutionPath();
    
    logTestNameToCocoReport(squishinfo.testCase, execution);

    在 Python 中

    execution = getExecutionPath()
    
    logTestNameToCocoReport(squishinfo.testCase, execution)
  3. 在您脚本结束时,在关闭 AUT(例如,通过单击 文件 > 退出)后,调用以下函数
    logTestResultsToCocoReport(test, execution);

    在 Python 中

    logTestResultsToCocoReport(test, execution)
  4. 如果您的 AUT 突然关闭或脚本发生错误,添加 trycatchfinally 可确保您的结果 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 公司的商标。所有其他商标均为其各自所有者的财产。