• GoogleTest测试最大子数组和的C++程序


    简介:

    全文分成三部分:第一部分介绍GoogleTest框架;第二部分是测试;第三部分是个人感想。

    继续吐槽博客园的Markdown。。不如其他博客的好用,格式很容易乱。。。。

    终于调整好格式了。。。。。。博客园的markdown 好low low low啊

    GoogleTest测试框架基本使用方法:

    首先,给出官方文档的链接。以下内容主要是我自己翻译的官方文档并结合自己的理解,总结出的基本要点。如果想要对技术更深入的了解,建议还是去看官方文档。最后说明一下,这里的内容仅仅在Linux系统的上执行过,其他的系统应该也是按照类似的步骤进行。Linux下关于如何安装GoogleTest框架,请参考我在CSDN的这篇博客

    基本概念

    使用GoogleTest要先从学习写断言开始,断言用于检测一个测试条件是否为真。断言的结果有三种情况:success, nonfatal failure, fatal failure。如果 fatal failure出现,它将会打断当前的函数;否则程序会正常运行。
    一个测试实例可以包含多个测试,我们需要把这些测试组织成合理的结构。当多个测试实例需要共享公共对象和或者子程序,我们可以把他们组织到一个测试类中。

    断言

    GoogleTest的断言是一种类似于函数调用的断言机制。我们可以在GoogleTest本身的断言消息后面定义自己的测试失败信息。下面说明几种不同断言方式:

    1. ASSERT_*产生fatal failures,直接终止当前函数的运行
    2. EXPECT_*: 产生nonfatal failures,不会终止当前函数运行
    3. EXPECT_*: 最常用的一种方式,可以允许报告产生一个或者多个failer

    为了提供自定义的失败信息,可以使用C++的stream流操作把字符输入到断言中,借助于<<操作符号。
    比如:

    ASSERT_EQ(x.size(), y.size()) << "Vectors x and y are of unequal length";
    
    for (int i = 0; i < x.size(); ++i) {
      EXPECT_EQ(x[i], y[i]) << "Vectors x and y differ at index " << i;
    }
    

    注意,任何可以写入流ostream的,都可被写入断言宏,比如C的字符串、string对象,甚至可以是广义上的字符(流):Windows的Unicode下的wchar_t、 TCHAR或者C++的std::string。所有流的输入都会转化成UTF-8的格式。

    基础断言判定:

    Name Academy score
    ASSERT_TRUE(condition) EXPECT_TRUE(condition) condition is true
    ASSERT_FALSE(condition) EXPECT_FALSE(condition) condition is false

    二元比较运算符:

    Fatal assertion Nonfatal assertion Verifies
    ASSERT_EQ(val1,val2); EXPECT_EQ(val1,val2); val1 == val2
    ASSERT_NE(val1,val2); EXPECT_NE(val1,val2); val1 != val2
    ASSERT_LT(val1,val2); EXPECT_LT(val1,val2); val1 < val2
    ASSERT_LE(val1,val2); EXPECT_LE(val1,val2); val1 <= val2
    ASSERT_GT(val1,val2); EXPECT_GT(val1,val2); val1 > val2
    ASSERT_GE(val1,val2); EXPECT_GE(val1,val2); val1 >= val2

    如果上述的一个测试失败,那么会打印出val1val2的值。

    Value参数必须与断言比较运算符兼容,否则有编译错误。这些断言机制可以使用用户自定义的结构,但是必须进行运算符重载 。如果使用了自定义结构,我们最好使用ASSERT_*(),这样不仅会输出比较结果,而且会输出操作数。

    ASSERT_EQ()会进行指针比较, 如果使用C风格字符串,那么比较的是地址!!如果要比较值的话,使用ASSERT_STREQ(), 如果判断C字符串是否是NULL,使用ASSERT_STREQ(NULL, c_string)。 如果比较string,那么使用ASSERT_EQ

    字符串比较:

    在这里,比较的是C风格的字符串,如果想要比较string对象,请使用EXPECT_EQ EXPECT_NE等,而不是下面的。

    Fatal assertion Nonfatal assertion Verifies
    ASSERT_STREQ(str1,str2); EXPECT_STREQ(str1,str2); the two C strings have the same content
    ASSERT_STRNE(str1,str2); EXPECT_STRNE(str1,str2); the two C strings have different content
    ASSERT_STRCASEEQ(str1,str2); EXPECT_STRCASEEQ(str1,str2); the two C strings have the same content, ignoring case
    ASSERT_STRCASENE(str1,str2); EXPECT_STRCASENE(str1,str2); the two C strings have different content, ignoring case

    简单的测试实例:

    创建一个test:

    1. 使用TEST()宏定义和命名一个特是函数,这是一个普通的无返回值的C++函数。
    2. 在函数内部,可以包含任何我们想要添加的C++条件,使用GoogleTest断言去检查相关的值。
    3. 测试的结果取决于内部的断言机制。如果有任何的测试失败(不管是fatally还是non-fatally),或者测试中断,那么整个测试失败;否则测试成功。
    TEST(testCaseName, testName) {
     ... test body ...
    }
    

    testCaseName是 test case的名字,testName是test case内部测试的名称。两者的名称必须是合法的C++标识符,不允许有**下划线( _ ) **。不同的test case的内部测试可以有相同的独立的名字。

    int Factorial(int n); // Returns the factorial of n
    
    // Tests factorial of 0.
    TEST(FactorialTest, HandlesZeroInput) {
      EXPECT_EQ(1, Factorial(0));
    }
    
    // Tests factorial of positive numbers.
    TEST(FactorialTest, HandlesPositiveInput) {
      EXPECT_EQ(1, Factorial(1));
      EXPECT_EQ(2, Factorial(2));
      EXPECT_EQ(6, Factorial(3));
      EXPECT_EQ(40320, Factorial(8));
    }
    

    GoogleTest通过test case组织测试结果,因此逻辑相关的测试必须在一个test case中;换句话说,TEST()的第一个参数必须相同。上面例子的HandlesZeroInputHandlesPositiveInput都属于FactorialTest这个测试实例。

    Test Fixtures: 对不同的数据使用相同的测试数据配置

    Test Fixture允许我们使用相同对象配置进行不同的测试。
    具体步骤:

    1. ::testing::Tes派生一个类。使用是public:或者protected:,因为我们想要在子类中获取fixture members
    2. 在派生类的内部,声明我们想使用的任何对象
    3. 如果有必要, 通过使用默认构造函数或者SetUp()函数为每个测试准备测试对象。
    4. 如果有必要, 使用一个析构函数或者TearDown()函数来释放构造函数或者SetUp()所申请的资源。 想要理解在什么时候使用SetUp()TearDown()函数, 阅读这篇文章.
    5. 如果必要的话,可以定义子程序,让测试之间共享。

    当使用一个fixture时,应该使用TEST_F()而不是TEST(),因为前者会让我们获取一个test fixture的对象或者子程序。比如:

    TEST_F(test_case_name, test_name) {
     ... test body ...
    }
    

    TEST()类似,第一个参数是test case的名字。但是第二参数必须是test fixture类的名字。

    C++的宏系统不允许我们创建一个单独的宏来处理各种类型的test。这样做会有编译错误。

    对于每个使用TEST_F()定义的测试,Google Test会:

    1. 在运行期间创建一个新的test fixture
    2. 通过SetUp()立刻进行初始化
    3. 运行test
    4. 通过TearDown()函数清理
    5. 删除这个test fixture。注意:一个test中的不同test拥有不同的test fixture对象,Google Test总是在创建下一个test fixture之前删除当前的test fixture。对于多个test,Google Test不会重复使用相同的test fixture。一个test的任何更改都不会影响其他的test。

    比如,现在编写一个FIFO队列的测试,队列的实现方式:

    template <typename E> // E is the element type.
    class Queue {
     public:
      Queue();
      void Enqueue(const E& element);
      E* Dequeue(); // Returns NULL if the queue is empty.
      size_t size() const;
      ...
    };
    

    首先,定义一个fixture类。假设该类的名称为Foo, 按照惯例,我们应该给fixture命名为FooTest

    class QueueTest : public ::testing::Test {
     protected:
      virtual void SetUp() {
        q1_.Enqueue(1);
        q2_.Enqueue(2);
        q2_.Enqueue(3);
      }
    
      // virtual void TearDown() {}
    
      Queue<int> q0_;
      Queue<int> q1_;
      Queue<int> q2_;
    };
    

    本例子中,没用TearDown()函数,因为没有资源需要释放。

    现在 ,使用TEST_F()测试这个fixture:

    // 用于测试是否是空队列
    TEST_F(QueueTest, IsEmptyInitially) {
      EXPECT_EQ(0, q0_.size());
    }
    
    // 测试出队的工作状态
    TEST_F(QueueTest, DequeueWorks) {
      int* n = q0_.Dequeue();
      EXPECT_EQ(NULL, n);  // 判断相等的情况
    
      n = q1_.Dequeue();
      ASSERT_TRUE(n != NULL);
      EXPECT_EQ(1, *n);
      EXPECT_EQ(0, q1_.size());
      delete n;
    
      n = q2_.Dequeue();
      ASSERT_TRUE(n != NULL);
      EXPECT_EQ(2, *n);
      EXPECT_EQ(1, q2_.size());
      delete n;
    }
    

    ASSERT_EXPECT_的区别在前面的文章提到了,这里不在赘述。

    以上面的例子,说明GoogleTest的测试步骤:

    1. Google Test创建一个QueueTest对象, 我们称之为t1
    2. t1.SetUp()初始化t1
    3. t1进行第一个测试IsEmptyInitially
    4. 测试完成后t1.TearDown()清理所有的资源。
    5. t1析构。
    6. 上述的步骤重复的在另一个QueueTest对象上执行。(DequeueWorks开始执行上述步骤)

    建立一个测试:

    TEST()TEST_F()会跟随Google自动进行注册,如果想要执行,我们不需要重新列出所有定义的测试。

    在定义测试之后,使用RUN_ALL_TEST()。如果测试成功则返回0,否则返回0.,执行这个语句的时候,所有的链接单元都会被测试,它们可以是不同的test case的。
    一般步骤:

    1. 保存所有Google Test标志的状态。
    2. 为第一个测试创建一个test feature对象。
    3. 通过SetUp()函数初始化test
    4. fixture test开始在该对象上执行
    5. 测试结束后,通过TearDown()释放资源
    6. 删除fixture
    7. 恢复Google Test的所有标志状态
    8. 其他的test重复地执行上述步骤。

    注意,如果构造函数在第2步产生了fatal failure,那么3-5步会自动跳过。同样的,3产生了fatal failure,第4步跳过。

    注意: 我们必须返回RUN_ALL_TEST()的值,否则gcc会给出编译错误。也就是说,主函数必须返回RUN_ALL_TEST()的值!而且RUN_ALL_TEST()只能执行一次!!

    编写主函数:

    Google Test的官方文档给出了一个测试模板:

    #include "this/package/foo.h"
    #include "gtest/gtest.h"
    
    namespace {
    
    // The fixture for testing class Foo.
    class FooTest : public ::testing::Test {
      protected:
        // You can remove any or all of the following functions if its body
        // is empty.
    
        FooTest() {
            // You can do set-up work for each test here.
        }
    
        virtual ~FooTest() {
            // You can do clean-up work that doesn't throw exceptions here.
        }
    
        // If the constructor and destructor are not enough for setting up
        // and cleaning up each test, you can define the following methods:
    
        virtual void SetUp() {
            // Code here will be called immediately after the constructor (right
            // before each test).
        }
    
        virtual void TearDown() {
            // Code here will be called immediately after each test (right
            // before the destructor).
        }
    
        // Objects declared here can be used by all tests in the test case for Foo.
    };
    
    // Tests that the Foo::Bar() method does Abc.
    TEST_F(FooTest, MethodBarDoesAbc) {
        const string input_filepath = "this/package/testdata/myinputfile.dat";
        const string output_filepath = "this/package/testdata/myoutputfile.dat";
        Foo f;
        EXPECT_EQ(0, f.Bar(input_filepath, output_filepath));
    }
    
    // Tests that Foo does Xyz.
    TEST_F(FooTest, DoesXyz) {
        // Exercises the Xyz feature of Foo.
    }
    
    }  // namespace
    
    int main(int argc, char **argv) {
        ::testing::InitGoogleTest(&argc, argv);
        return RUN_ALL_TESTS();
    }
    

    ::testing::InitGoogleTest()用于解析Google Test flags的命令行,并且一处所有已经被识别的标志,具体的用法请参照这个文档

    Visual C++用户须知!!

    因为本人不用vc++,因此各位用VC的dalao就自己看官方文档吧。。。。。。

    测试代码

    以最大子数组线性时间求和问题为例,介绍GoogleTest的测试框架。给出假期刷题时PAT1007原题目链接,并附上AC的代码,算法的原理就不在赘述了:

    #include <bits/stdc++.h>
    using namespace std;
    int num[10005], N;
    
    int main() {
        cin >> N;
        bool flag = true;
        for(int i = 0; i < N; ++i) {
            cin >> num[i];
            if(num[i] >= 0) {
                flag = false;
            }
        }
        // 注意sum初始化要小于0
        int a = 0, b = 0, sum = -1, tmp_sum = 0, tmp_a = 0, tmp_b = 0;
        while(tmp_b < N) {
            tmp_sum += num[tmp_b];
            if(tmp_sum > sum) { // 更替区间范围
                sum = tmp_sum;
                a = tmp_a;
                b = tmp_b;
            }
            if(tmp_sum < 0) {     // 重新开始起点
                tmp_sum = 0;
                tmp_a = tmp_b + 1;
            }
            ++tmp_b;
        }
        if(flag) {
            cout << 0 << " " << num[0] << " " << num[N - 1];
        } else {
            cout << sum << " " << num[a] << " " << num[b];
        }
        return 0;
    }
    

    但是,上述代码不好直接测试,因此,把核心功能分离出来写成函数,同时自定义结构体作为函数的返回值。改进后的代码如下:

    实际可能出现的情况有下面几种:

    1. 全是负数:左右标记分别是区间范围,子数组之和输出0
    2. 有最大子数组,且最大子数组唯一:输出最大子数组的范围和所有元素之和
    3. 有多个最大子数组:只输出第一个最大子数组,格式同2

    本次测试代码内容较少,就当做是熟悉环境的练习了。。。。。。。。。。。。。。。

    代码的Github地址:https://github.com/StudentErick/software_ware_homework

    #include <gtest/gtest.h>
    
    class Node {
      public:
        Node(): sum(0), l(0), r(0) {} // 初始化
        int sum, l, r;   // 区间和、左右范围,从0开始
    };
    
    Node maxFun(int arr[], int N, bool flag) {
        Node node;
        int a = 0, b = 0, sum = -1, tmp_sum = 0, tmp_a = 0, tmp_b = 0;
        while(tmp_b < N) {
            tmp_sum += arr[tmp_b];
            if(tmp_sum > sum) { // 更替区间范围
                sum = tmp_sum;
                a = tmp_a;
                b = tmp_b;
            }
            if(tmp_sum < 0) {     // 重新开始起点
                tmp_sum = 0;
                tmp_a = tmp_b + 1;
            }
            ++tmp_b;
        }
        if(flag) {
            node.l = 0;
            node.r = 9;
            node.sum = 0;
        } else {
            node.l = a;
            node.r = b;
            node.sum = sum;
        }
    
        return node;
    }
    
    // 需要全局重载
    bool operator==(Node a, Node b) {
        return a.sum == b.sum && a.l == b.l && a.r == b.r;
    }
    
    // 全是负数的测试情况
    int num1[10] = { -1, -2, -5, -2, -8, -6, -9, -3, -10, -4};
    // 只有一个最大子数组,左右范围是2 7,和是25
    int num2[10] = {1, -14, 5, 6, 8, 3, -1, 4, -10, 4};
    // 有多个,在这里用两个测试,左右范围应该是2 4 和是19
    int num3[10] = {1, -14, 5, 6, 8, -300, 5, 6, 8, -4};
    
    TEST(MYTEST, IsOk) {
        Node n1, n2, n3;
        n1.l = 0;
        n1.r = 9;
        n1.sum = 0;
        n2.l = 2;
        n2.r = 7;
        n2.sum = 25;
        n3.l = 2;
        n3.r = 4;
        n3.sum = 19;
    
    
        ASSERT_EQ(n1, maxFun(num1, 10, true));
        ASSERT_EQ(n2, maxFun(num2, 10, false));
        ASSERT_EQ(n3, maxFun(num3, 10, false));
    }
    
    int main(int argc, char **argv) {
        ::testing::InitGoogleTest(&argc, argv);
        return RUN_ALL_TESTS();
    }
    
    

    由于是自定义的返回值,所以根据官方文档的建议,使用ASSERT_*。测试通过结果如下:

    假设更改n3l属性为4,那么测试失败的结果如下:

    这里写图片描述
    其中,有相应的不匹配提示。由于本测试比较简单,所以没有使用到test fixture的技术。如果需要,直接套用模板即可。更高级的功能可以参考

    一些感想:

    本次测试仅通过官方文档进行学习。官方的参考文档是最佳的参考资料。尤其是对于我们不熟悉的技术领域,更应该通过阅读有关文档进行学习。英文应该作为技术开发人员的一项基本能力,不仅仅是为了考研或者是所谓的四六级。很多最新的资料或者比较高端的技术或者一些顶级期刊的论文等,几乎没有中文版的,因此我们更应该不断提高自己的英文水平。同时,要怀着积极的心态去拥抱新的技术和变化,善于利用工具提高开发或者测试效率。

  • 相关阅读:
    【题解】P2569 [SCOI2010]股票交易
    【题解】P3354 [IOI2005]Riv 河流
    入职阿里蚂蚁三个月有感
    搞懂G1垃圾收集器
    MySql分库分表与分区的区别和思考
    Kafka源码分析及图解原理之Broker端
    Kafka源码分析及图解原理之Producer端
    Oracle GoldenGate mysql To Kafka上车记录
    从动态代理到Spring AOP(中)
    从动态代理到Spring AOP(上)
  • 原文地址:https://www.cnblogs.com/Erick-Lv/p/8675734.html
Copyright © 2020-2023  润新知