编写检查定时行为的快速测试

Writing fast tests that check timed behavior

本文关键字:测试 检查 定时      更新时间:2023-10-16

我有下面的类(当然,为示例简化了):

class file_handler_c {
public:
  err_t init(int msecs);
  err_t write(byte* buffer);
}

这个类要求init在任何write之前被调用,然后你可以使用这个类来write到一个文件。但是,在msecs毫秒之后,write停止写入文件并返回一个错误。

我的问题是——如何为这种行为创建一个快速单元测试?任何足够小的值都会创建一个不确定的测试,有时会因为机器上运行的其他进程而失败。但是,我希望测试尽可能快,不包括任何sleep或类似的。我正在使用谷歌测试和谷歌模拟。

首先,我必须稍微转移一下你的实际问题。请不要创建这样的接口。无论何时创建对象,在其生命周期结束之前,它的所有函数都必须是可调用的。非正式的生命周期约束,如"init()必须在write()之前调用",或者更糟糕的是"总是在析构函数之前调用close()",编译器无法检查,因此容易出错。

因此,我不认为你应该在测试中解决这个问题,而应该在测试代码的设计中解决。无论何时编写测试很难,可以肯定的是,您的接口存在缺陷。

我的建议是把计时和写作分开。创建一个定时器类,例如:

#include <memory>
#include <chrono>
template<typename T>
class Timer {
    private:
        std::chrono::milliseconds time_to_live;
        std::shared_ptr<T> timed_object;
    public:
        template<typename... Arg>
        Timer(std::chrono::milliseconds ttl, Arg... args):
          time_to_live{ttl},
          timed_object{std::make_shared<T>(args...)} {};
        std::weak_ptr<T> get() { return {timed_object}; };
        // ...
        // when the defined time is over, timed_object will be set to nullptr somehow.
};

像这样使用:

#include <chrono>
#include <iostream>
#include <fstream>
int main(int, char**)
{
    using namespace std::literals::chrono_literals;
    auto timed_ostream = Timer<std::ofstream>{42ms, "filename"};
    if( !timed_ostream.get().expired() ) {
       // os is a shared_ptr. That guarantees, that the ostream will not
       // be closed while you are still writing.
       auto os = timed_ostream.get().lock();
       (*os) << "whatever";
    } // now os will be destroyed. The ofstream might be destroyed
      // as well now, when the 42ms are over.
} // OK, here we destroy the timer and therefore the ofstream,
  // if it is still alive.

有了这个接口,你可以很容易地编写一个简单的测试用例,而不是ostream,例如int:

#include <chrono>
#include <cassert>
using namespace std::literals::chrono_literals;
void test_timed_object_valid_after_init()
{
    auto clock = std::chrono::high_resolution_clock{};
    auto start = clock.now();
    auto timed_int = Timer<int>{2000ms,42}; // valid for 2000ms
    assert(timed_int.get().expired()); // should still be here
} // The timer will be destroyed here. That destroys the shared_ptr
  // and the object as well. The long lifetime does not matter.
void test_timed_object_invalid_after_time()
{
    auto clock = std::chrono::high_resolution_clock{};
    auto start = clock.now();
    auto timed_int = Timer<int>{1ms,42}; // valid for 1ms
    // you did not want sleep(), so we do busy waiting. 
    // Prefer usleep() instead.
    // busy wait 1ms as exactly as possible.
    while( clock.now() - start < 1ms ) {}
    assert(timed_int.get().expired()); // should be gone now.
}

注意,这里每个测试用例只检查一个场景。不要尝试在一个测试用例中测试这两个需求。那么,你要么必须使用较长的生命周期来检查对象在初始化后是否安全存在,要么选择较短的生命周期来检查对象在初始化后是否消失。

注意:这篇文章中的所有代码都应该可以编译,但当然可能在某些地方仍然存在错误。这些留给学生作为练习;-)