使用GTest以正确的方式多次重复多线程测试的方法是什么?

3

我想使用Google Test测试一个Server.AcceptRequest方法的行为:

class Server {
public:
    // Clients can call this method, want to test that it works
    Result AcceptRequest(const Request& request) {
        queue_.Add(request);
        ... blocks waiting for result ...
        return result;
    }
private:
    // Executed by the background_thread_;
    void ProcessRequestsInQueue() {
        while (true) {
            Process(queue_.PopEarliest());
        }
    }

    MySynchronizedQueue queue_;
    std::thread background_thread_ = thread([this] {ProcessRequestsInQueue();});
};

该方法接受客户端请求,将其排队,阻塞等待结果,并在结果可用时返回结果。
当后台线程处理来自队列的相应请求时,结果就可用了。
我有一个测试,如下所示:
TEST(ServerTest, TwoRequests) {
    Server server;
    Result r1 = server.AcceptClientRequest(request1);
    Result r2 = server.AcceptClientRequest(request2);
    ASSERT_EQ(r1, correctResultFor1);
    ASSERT_EQ(r2, correctResultFor2);
}

由于实现Server类涉及多个线程,因此此测试可能在一次尝试中通过,但在另一次尝试中失败。为了增加捕获错误的机会,我运行多次测试:

TEST_P(ListenerTest, TwoRequests) {
  ... same as before ...
}
INSTANTIATE_TEST_CASE_P(Instantiation, ServerTest, Range(0, 100));

但现在make test命令将每个参数化实例视为单独的测试,在日志中,我看到了100个测试:

Test 1: Instantiation/ServerTest.TwoRequests/1
Test 2: Instantiation/ServerTest.TwoRequests/2
...
Test 100: Instantiation/ServerTest.TwoRequests/100

鉴于我不使用参数值,是否有一种重写测试代码的方法,使得make test命令记录执行了100次单个测试,而不是100个测试


1
在我看来,你根本不应该有非确定性的单元测试。单元测试是用来测试你的功能(而不是环境)的。如果你觉得围绕着HTTP监听器的逻辑足够重要,需要进行单元测试,那么可以使用一个测试完全可控的模拟监听器。 - undefined
那不是传统意义上所指的单元测试。单元测试是测试代码的离散单元,而不是系统集成。 - undefined
这很令人困惑。为什么一开始就会有非确定性的代码呢?为什么结果是非确定性的?这没有道理。如果你所说的“非确定性”实际上是指“在不同的外部条件下表现不同”,那么这就不是“非确定性”,你应该简单地复制/模拟这些条件并测试这些情况。或者也许你所说的“非确定性”是指“涉及某种程度的随机性”,那么在这种情况下,你可以运行测试数百次,收集结果并进行一些统计检查。 - undefined
测试确定性行为更好。例如:你真的需要检查ID吗? - undefined
@freakish 添加了解释。 - undefined
显示剩余5条评论
3个回答

7
简单回答:在执行测试时使用 --gtest_repeat 就可以做到这一点(默认值为1)。
更长的回答:单元测试不应该用于这种类型的测试。GTest是按设计线程安全的(正如他们在README中所述),但这并不意味着它是执行此类测试的好工具。也许这是实际开始开发真正的集成测试的好起点,我强烈推荐Python的behave框架用于此目的。

gtest_repeat可以添加到CMake文件中吗? - undefined
从未尝试过这个,所以不确定。虽然它是运行时配置,就像 --gtest_filter,所以我怀疑,但也许CMake支持它。 - undefined
1
即使如此,我建议有一个CI任务只运行所有测试一次(即不使用--gtest_repeat),另一个CI任务运行这个特定的测试多次(即使用--gtest_repeat--gtest_filter=ListenerTest.TwoRequests),因为其他所有测试都应该很小,并且多次运行它们没有太多意义。为需要多次运行的测试添加特殊的前缀/后缀(如BUGCHECK)可能有助于筛选出这些测试。 - undefined
1
注意:gtest_repeat将重复所有的测试。如果之前的迭代中有错误,最后不会报告这些错误,只有在最后一次迭代成功时才会报告。您需要手动滚动或扫描所有运行和之前的迭代。最终的摘要行只包括最后一次迭代的测试编号、失败情况和时间。请注意可能存在不稳定的测试。 - undefined

1
每个测试都应该在一个干净的状态下运行,不与其他测试共享状态。换句话说,如果一个测试因为在另一个测试之后运行而出现故障,那么你的生产代码可能会出现问题,但你的测试代码肯定有问题。
如果你想知道你的监听器能否接受多个请求,那么这就是你的测试。在测试中创建一个干净的监听器实例并运行一个循环,进行一系列调用。如果需要,可以为每个调用创建一个新线程(只需记住在测试结束时清理即可)。

1

依赖注入在这种情况下很有效。

构造 Server 对象时:

Server new_server_instance;

你构建了一个后台线程的真实实现:

std::thread background_thread_ = thread([this] {ProcessRequestsInQueue();});

目前的实现方式无法控制后台线程处理请求的时间。

相反,将后台线程作为依赖项传递:

class Server {
     Server(std::thread background_thread): background_thread_(background_thread) {}
}

Server new_server_instance(background_thread);

这样,在单元测试中,您可以构建一个传递虚假后台线程的服务器,该线程将按照您想要的任何顺序处理请求。 然后,您可以构建多个测试,使用不同的虚假后台线程测试所有潜在的竞争条件。

更多信息,请阅读:

https://en.wikipedia.org/wiki/Dependency_injection

什么是依赖注入?


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