使用Google Test进行数据驱动的单元测试

11
我目前正在使用谷歌的单元测试框架编写嵌入式应用程序的单元测试。现在我的老板对我测试时使用的数据(即我调用被测类方法的值)感到不满,因为这些数据是硬编码在测试中的。他要求从文件中读取这些数据。他的论点是这样做可以更容易地添加另一个以前被遗忘的边角案例的测试。虽然我没有很多单元测试经验,但迄今为止我并不是这样做的。所以我试图找出最好的方法来做这件事——即使这样做是个好主意。我很快就发现了数据驱动测试(DDT)的方法。
谷歌的单元测试框架有一个称为“Value-Parameterized Tests”的功能。使用它,我的测试夹具变成了一个模板类,我可以传递参数。然而,我认为这种方法存在一些问题:
我目前为每个测试类都创建了一个装置。但是,由于每个方法需要一组不同的参数,所以我需要为每个测试方法创建一个装置。这将会增加很多额外的工作。
  • 就我而言,我只能传递一个参数。由于我的测试需要几个参数(方法的所有参数加上预期的结果) ,因此这将要求我传递诸如向量或映射之类的东西。再次构建和检索听起来像是很多工作。
  • 我想象中的谷歌测试框架应该会让这变得更容易。然而,他们写道:

    当您希望通过各种输入(也称为数据驱动测试)来测试代码时,有用的是value-parameterized tests。此功能很容易被滥用,请在使用时行使您的良好判断力!

    此外,还有这篇博客文章 TotT: Data Driven Traps,也警告我不要滥用数据驱动单元测试。

    因此,我的问题归结为:

    • 进行数据驱动的单元测试是否是个好主意?
    • 如何使用谷歌测试框架进行数据驱动的单元测试?
    我并不是真正地被绑定在googletest上,基本上可以自由选择任何我喜欢的框架。但是,我在googletest FAQ的一个条目中发现了以下声明:
    “Google Test目前对于......一般数据驱动测试的支持还不够好。我们希望能够尽快在这个领域取得改进。”
    1个回答

    11

    GTest支持此功能-但也许他们不知道...

    使用testing :: ValuesIn - 就像在这个简化的示例中:

    class SomeTests : public TestWithParam<int>
    {
    public:
    
    };
    
    TEST_P(SomeTests, shouldBePositive)
    {
        ASSERT_GT(GetParam(), 0);
    }
    

    而获取输入流中的值的方法是:

    std::ifstream inputValuesFromFile("input.txt");
    INSTANTIATE_TEST_CASE_P(FromFileStream,
                            SomeTests,
                            ValuesIn(std::istream_iterator<int>(inputValuesFromFile), 
                                     std::istream_iterator<int>()));
    

    如果要使用比"int"类型更复杂的类型,则需要为其编写一个运算符">>" - 例如:

    struct A
    {
        int a;
        std::vector<int> b;
    }
    std::istream& operator >> (std::istream& is, A& out)
    {
         std::size_t bSize;
         if ((is >> A.a) && (is >> bSize))
         {
             out.b.reserve(bSize);
             while (bSize-- > 0)
             {
                 int b;
                 if (!(is >> b))
                    break;
                 out.b.push_back(b);   
             }
         }
         return is;
    }
    

    当然,在更复杂的情况下,考虑使用类似 XML(JSON?)格式和比 std::istream_iterator<T> 更专业的迭代器。


    对于类似 XML 的格式,您可以考虑使用以下方案(这只是非常假设性的代码 - 我脑海中没有任何这样的库):

    SomeLib::File xmlData("input.xml");
    
    class S1BasedTests : public TestWithParam<S1>
    {};
    
    TEST_P(S1BasedTests , shouldXxxx)
    {
        const S1& s1 = GetParam();
        ...
    }
    auto s1Entities = file.filterBy<S1>("S1");
    INSTANTIATE_TEST_CASE_P(S1,
                            S1BasedTests,
                            ValuesIn(s1Entities.begin(), s1Entities .end()));
    

    // 对于您想要的任何类型S1等等


    如果市场上没有这样的C ++友好库(我搜索了2分钟也没有找到)-那么可能会有类似以下的东西:

    SomeLib::File xmlFile("input.xml");
    
    struct S1BasedTests : public TestWithParam<SomeLib::Node*>
    {
       struct S1 // xml=<S1 a="1" b="2"/>
       {
           int a;
           int b;
       };
       S1 readNode()
       {
            S1 s1{};
            s1.a = GetParam()->getNode("a").getValue<int>();
            s1.b = GetParam()->getNode("b").getValue<float>();
            return s1;
       }
    };
    
    TEST_P(S1BasedTests , shouldXxxx)
    {
        const S1& s1 = readNode();
        ...
    }
    INSTANTIATE_TEST_CASE_P(S1,
                            S1BasedTests ,
                            ValuesIn(xmlFile.getNode("S1").getChildren()));
                            // xml=<S1s> <S1.../> <S1.../> </S1>
    

    // 对于任何节点类型(如S1)等等


    1
    是的,这就是我所说的“值参数化测试”功能。但这变得非常复杂,不是吗?我需要为每个受测试方法单独设置一个夹具(因为签名不同)。并且每个夹具都需要自己的operator>>和结构体。 - sigy
    1
    正如我在答案中写的那样 - 请搜索一些C++的XML库 - 以获取来自XML的istream。有了这样的XML解析器 - 为每种类型的XML数据定义参数化测试套件。搜索像这个问题http://stackoverflow.com/questions/12338476/how-to-load-xml-document-from-stdistream这样的问题,或者只需问另一个关于如何从一个文件获取各种结构体 - 或者有多个文件...我正在向我的答案添加一些更新。 - PiotrNycz

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接