zoukankan      html  css  js  c++  java
  • 《实时控制软件设计》之单元测试

    《构建之法》第2章讨论了个人技术和流程,一个复杂的软件都是由若干个模块组成的,提高个人的技战术能力首先从编写一个稳定的模块开始,最基本的模块就是一个类。要保证自己提交的模块代码的质量得到保证,需要进行单元测试。2.1.2节给出了好的单元测试的标准:

    • 在最基本的功能/参数上验证程序的正确性。
    • 必须由最熟悉代码的人(程序作者)来写。
    • 单元测试之后,机器状态保持不变。
    • 单元测试要快。
    • 单元测试应该产生可重复、一致的结果。
    • 单元测试的独立性--不依赖于别的测试。
    • 单元测试应该覆盖所有代码路径。
    • 单元测试应该集成到自动测试的框架中。
    • 单元测试必须和产品代码一起保存和维护。

    参见单元测试 & 回归测试

    本课程作业都基于C++代码编写,所以单元测试工具我们选用CPPUNIT,下面我们介绍使用方法。
    要使用CPPUNIT库进行单元测试,首先需要在你的开发环境中安装CPPUNIT库,比如在Ubuntu Linux环境下,只需要执行如下命令

    sudo apt-get install libcppunit-dev
    

    就会把CPPUNIT的库文件及头文件安装到Linux的系统目录中,接下来在自己的测试代码中只要include相关的头文件,直接使用CPPUNIT函数了。

    无论采用何种编译环境,总可以通过下载CPPUNIT的源代码(下载地址)进行编译和编译环境内的路径参数设置来完成环境配置。

    可以通过编译如第1个最简单的例程来测试CPPUNIT是否安装好:

    //mytest.cpp
    #include <iostream>
    #include <cppunit/TestCase.h>
    
    class MyTest:public CppUnit::TestCase
    {
    public:
        MyTest(std::string name): CppUnit::TestCase(name){}
    
        void runTest()
        {
            CPPUNIT_ASSERT(1 == 1);
            CPPUNIT_ASSERT_DOUBLES_EQUAL(2.11, 2.13, 0.01); 
        }
    
    };
    
    int main()
    {
        MyTest test1("Test1_Name");
        
        std::cout << "This is the test: " << test1.getName() << std::endl;
        std::cout << "The test has number:" << test1.countTestCases() << std::endl;
        
        test1.runTest();    
        
        return 0;
    }
    

    在Ubuntu下可以用命令行编译该程序:

    g++ -o mytest mytest.cpp -I/opt/local/include -L/opt/locallib -lcppunit -ldl
    

    编译通过后运行该程序,会显示结果如下:

    接下来我们分析下mytest.cpp的代码,里面定义了一个测试用例类MyTest,继承自CPPUNIT的TestCase类,并在runTest中定义了两个测试宏:
    CPPUNIT_ASSERT(1 == 1);
    CPPUNIT_ASSERT_DOUBLES_EQUAL(2.11, 2.13, 0.01);

    第一个是调用CPPUNIT_ASSERT(condition),判断condition是否为真。
    第二个是调用CPPUNIT_ASSERT_DOUBLES_EQUAL(expected, actual, delta),判断两个浮点数expected和actual之间的差是否大于delta。
    在主程序中创建MyTest的实例test1,并执行测试函数test1.runTest()。运行结果显示第二项测试没有通过。

    上面是最简单的单元测试代码,但实际的单元测试往往不是直接使用 TestCase类,而是用TestFixture类,TestFixture类拥有TestSuite,每个TestSuite又可以拥有多个TestCase。下面是第2个示例代码:

    #include <cppunit/extensions/HelperMacros.h>
    #include <cppunit/ui/text/TestRunner.h>
    
    
    class MyTests : public CppUnit::TestFixture
    {
    	CPPUNIT_TEST_SUITE( MyTests );
    	CPPUNIT_TEST( testAdd );
    	CPPUNIT_TEST( testEquals );
    	CPPUNIT_TEST_SUITE_END();
    
    	double m_value1;
    	double m_value2;
    
    public:
    
    	void setUp()
    	{
    	  m_value1 = 2.0;
    	  m_value2 = 3.0;
    	}
    
            void tearDown()
            {
            }
    
    	void testAdd()
    	{
    	  double result = m_value1 + m_value2;
    	  CPPUNIT_ASSERT( result == 6.0 );
    	}
    
    
    	void testEquals()
    	{
    	  long* l1 = new long(12);
    	  long* l2 = new long(12);
    
    	  CPPUNIT_ASSERT_EQUAL( 12, 12 );
    	  CPPUNIT_ASSERT_EQUAL( 12L, 12L );
    	  CPPUNIT_ASSERT_EQUAL( *l1, *l2 );
    
    	  delete l1;
    	  delete l2;
    
    	  CPPUNIT_ASSERT( 12L == 12L );
    	  CPPUNIT_ASSERT_EQUAL( 12, 13 );
    	  CPPUNIT_ASSERT_DOUBLES_EQUAL( 12.0, 11.99, 0.5 );
    	}
    };
    
    int main()
    {
      CppUnit::TextUi::TestRunner runner;
      runner.addTest( MyTests::suite() );
      runner.run();
      return 0;
    }
    

    分析下这个程序,我们发现在类MyTests的定义中采用了如下宏:

    	CPPUNIT_TEST_SUITE( MyTests );
    	CPPUNIT_TEST( testAdd );
    	CPPUNIT_TEST( testEquals );
    	CPPUNIT_TEST_SUITE_END();
    

    其作用就是定义了两个TestCase: testAdd和testEquals,并把这两个TestCase添加到一个叫MyTests的TestSuite中去。
    在主程序中实例化了一个TestRunner类对象runner,把MyTests这个测试用例集合添加到runner,然后调用runner.run()就自动执行所有的测试用例了。
    从这个程序中,我们也可以体会到面向对象编程的一些思想方法。每次具体的单元测试的内容都是变化的,但是单元测试的基本原理和流程是不变的,我们的程序设计应该把不变的部分和变化的部分有效地区隔开来。在上面程序中,TestRunner对象相当于工厂里的质检员,他只按照标准的测试流程工作,所以我们不需要重新定义它,而是直接用CPPUNIT的类定义实例化一个对象,但TestRunner具体执行什么测试取决于我们提供给它什么测试用例集,所以我们只需要定义一个具体的测试用例集,并把该测试用例集传递给TestRunner,TestRunner通过调用它的标准作业流程run(),去执行每一个测试用例并返回结果。所以我们看主函数中的三行代码:

      CppUnit::TextUi::TestRunner runner;
      runner.addTest( MyTests::suite() );
      runner.run();
    

    第1行和第3行都不需要随着测试用例集的变化而变化,只有在第2行中,要把自定义的测试用例集名传递给TestRunner。
    为了实现自动化的单元测试,我们希望当开发人员编写新的模块并增加或修改单元测试程序时,测试框架程序不随之变化。我们来看看KDL库里是如何实现这一点的。
    framestest.hppframestest.cpp是一个具体的单元测试代码,里面具体的测试用例用来测试KDL的frames模块,如下面的一个测试宏:

    CPPUNIT_ASSERT_DOUBLES_EQUAL((R*v).Norm(),v.Norm(),epsilon);
    

    是为了测试一个向量v进行旋转操作R后是否保持模不变,从数学上讲模是完全不变的,但数值运算上肯定有精度问题,这里用epsilon来测试运算精度是否在epsilon范围内。

    整个代码结构和上面第2个例程基本类似,稍有不同的是在framestest.cpp的开始部分,有一行代码

    CPPUNIT_TEST_SUITE_REGISTRATION( FramesTest );
    

    在KDL的tests目录中,有多个与FramesTest类似的单元测试代码,其结构和风格都类似,如JacobianTest,在其cpp实现文件中也有一行代码:

    CPPUNIT_TEST_SUITE_REGISTRATION(JacobianTest);
    

    test-runner.cpp则给出了测试框架程序:

    #include <cppunit/XmlOutputter.h>
    #include <cppunit/extensions/TestFactoryRegistry.h>
    #include <cppunit/ui/text/TestRunner.h>
    #include <iostream>
    #include <fstream>
    
    int main(int argc, char** argv)
    {
        // Get the top level suite from the registry
        CppUnit::Test *suite = CppUnit::TestFactoryRegistry::getRegistry().makeTest();
    
        // Adds the test to the list of test to run
        CppUnit::TextUi::TestRunner runner;
        runner.addTest( suite );
    #ifndef TESTNAME
        std::ofstream outputFile(std::string(suite->getName()+"-result.xml").c_str());
    #else
        std::ofstream outputFile((std::string(TESTNAME)+std::string("-result.xml")).c_str());
    #endif
        // Change the default outputter to a compiler error format outputter
        runner.setOutputter( new CppUnit::XmlOutputter( &runner.result(),outputFile ) );
        
        // Run the tests.
        bool wasSucessful = runner.run();
    
        outputFile.close();
        // Return error code 1 if the one of test failed.
        return wasSucessful ? 0 : 1;
    }
    

    主函数中有几行代码用于创建了一个log文件用于记录单元测试的结果,有两行代码

        runner.addTest( suite );
        bool wasSucessful = runner.run();
    

    和第2个例子是完全相同的,不同的是在第2个例子中,我们需要直接把具体TestSuite名添加到runner中,但在这个框架程序中,使用了一行代码自动收集KDL所有的单元测试用例添加到suite中:

        CppUnit::Test *suite = CppUnit::TestFactoryRegistry::getRegistry().makeTest();
    

    这个就是设计模式中的工厂方法模式,每个具体的单元测试类通过CPPUNIT_TEST_SUITE_REGISTRATION宏把自己注册到工厂类中去,然后框架程序通过工厂类的方法自动创建所有的测试用例集对象,在项目开发过程中,test-runner.cpp作为测试框架程序不需要随着单元测试数量的变化进行代码的修改,可以自动执行并生成测试结果报告,这里也充分体现了设计模式的威力。

  • 相关阅读:
    BZOJ 1050 旅行
    BZOJ 1040 骑士
    BZOJ 1038 瞭望塔
    BZOJ 1037 生日聚会
    BZOJ 1823 满汉全席
    BZOJ 3091 城市旅行
    CF702E Analysis of Pathes in Functional Graph
    Luogu 2154 [SDOI2009]虔诚的墓主人
    Luogu 1268 树的重量
    Luogu 4867 Gty的二逼妹子序列
  • 原文地址:https://www.cnblogs.com/bingc/p/5059819.html
Copyright © 2011-2022 走看看