你怎么嘲笑加速计时器的时间?

时间:2013-01-07 07:46:01

标签: c++ unit-testing boost-asio

如果可能的话,你如何模拟在单元测试中触发升压定时器的时间?

例如,是否可以实现以下内容:

#include <iostream>
#include <boost/asio.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>

void print(const boost::system::error_code& /*e*/)
{
  std::cout << "Hello, world!\n";
}

int main()
{
    boost::asio::io_service io;        // Possibly another class needed here, or a way of setting the clock to be fake

    boost::asio::deadline_timer t(io, boost::posix_time::hours(24));
    t.async_wait(&print);

    io.poll();  // Nothing should happen - no handlers ready

    // PSEUDO-CODE below of what I'd like to happen, jump ahead 24 hours
    io.set_time(io.get_time() + boost::posix_time::hours(24));

    io.poll();  // The timer should go off

    return 0;
}

更新感谢您的所有答案,他们提供了很好的解决方案。我提供了自己的答案(SSCCE),但没有提供的帮助就无法做到。

4 个答案:

答案 0 :(得分:7)

basic_deadline_timer模板有traits参数,您可以使用该参数提供自己的时钟。 Boost Asio的作者有blog post展示了如何做到这一点。这是帖子中的一个例子:

class offset_time_traits
  : public asio::deadline_timer::traits_type
{
public:
  static time_type now()
  {
    return add(asio::deadline_timer::traits_type::now(), offset_);
  }

  static void set_now(time_type t)
  {
    offset_ =
      subtract(t, asio::deadline_timer::traits_type::now());
  }

private:
  static duration_type offset_;
};

typedef asio::basic_deadline_timer<
    boost::posix_time::ptime, offset_time_traits> offset_timer;

也许您可以在整个应用程序中使用offset_timer之类的内容,但在运行测试时只能调用set_now()

答案 1 :(得分:4)

据我所知,无法模仿时间变化或使用Boost模拟set the time。在扩展可用于解决此问题的一些技术之前,需要考虑以下几点:

  • Boost.Asio提供使用时钟的定时器,但不提供时钟,因为它们超出了Boost.Asio的范围。因此,与时钟相关的功能(如设置或仿真)不在Boost.Asio的功能范围内。
  • 单调时钟可以在内部使用。因此,时钟(模拟或实际)的变化可能不会产生期望的效果。例如,boost::asio::steady_timer不会受到系统时间更改的影响,使用epoll的反应器实现可能需要长达5分钟才能检测到系统时间的更改,因为它可以防止系统更改时钟。
  • 对于Boost.Asio计时器,更改过期时间将隐式取消WaitableTimerServiceTimerService要求的异步等待操作。此取消导致未完成的异步等待操作尽快完成,取消的操作将出现错误代码boost::asio::error::operation_aborted

尽管如此,根据正在测试的内容,有两种总体技术可以解决这个问题:

  • 缩放时间。
  • 包装类型。

缩放时间

缩放时间保留了多个计时器之间相同的总体相对流量。例如,具有1秒到期时间的计时器应在具有24小时到期的计时器之前触发。最小和最大持续时间也可用于其他控制。此外,缩放持续时间适用于不受系统时钟影响的定时器,如steady_timer

这是一个例子,其中应用了1小时= 1秒的比例。因此,24小时到期实际上是24秒到期。 另外,

namespace bpt = boost::posix_time;
const bpt::time_duration max_duration = bpt::seconds(24);
const boost::chrono::seconds max_sleep(max_duration.total_seconds());

bpt::time_duration scale_time(const bpt::time_duration& duration)
{
  // Scale of 1 hour = 1 seconds.
  bpt::time_duration value =
    bpt::seconds(duration.total_seconds() * bpt::seconds(1).total_seconds() /
      bpt::hours(1).total_seconds());
  return value < max_duration ? value : max_duration;
}

int main()
{
  boost::asio::io_service io;
  boost::asio::deadline_timer t(io, scale_time(bpt::hours(24)));
  t.async_wait(&print);
  io.poll();
  boost::this_thread::sleep_for(max_sleep);
  io.poll();
}

包装类型

有一些不同的位置可以引入新类型以获得一些所需的行为。

在所有这些情况下,重要的是要考虑到更改过期时间将隐式取消异步等待操作的行为。

包裹deadline_timer

包装deadline_timer需要在内部管理用户的处理程序。如果计时器将用户的处理程序传递给与计时器关联的服务,则当到期时间发生变化时,将通知用户处理程序。

自定义计时器可以:

  • 在内部(WaitHandler)将提供的user_handler_存储到async_wait()
  • 调用cancel()时,会设置内部标记以指示已取消(cancelled_)。
  • 聚合计时器。设置到期时间后,内部处理程序将传递给聚合计时器async_wait。无论何时调用内部处理程序,它都需要处理以下四种情况:
    • 正常超时。
    • 明确取消。
    • 将到期时间的隐式取消更改为时间。
    • 从到期时间的隐式取消更改为将来的时间。

内部处理程序代码可能如下所示:

void handle_async_wait(const boost::system::error_code& error)
{
  // Handle normal and explicit cancellation.
  if (error != boost::asio::error::operation_aborted || cancelled_)
  {
    user_handler_(error);
  }
  // Otherwise, if the new expiry time is not in the future, then invoke
  // the user handler.
  if (timer_.expires_from_now() <= boost::posix_time::seconds(0))
  {
    user_handler_(make_error_code(boost::system::errc::success));
  }
  // Otherwise, the new expiry time is in the future, so internally wait.
  else
  {
    timer_.async_wait(boost::bind(&custom_timer::handle_async_wait, this,
                      boost::asio::placeholders::error));
  }
}

虽然这很容易实现,但它需要理解定时器接口,足以模仿其前/后条件,除了您想要偏离的行为。测试中也可能存在风险因素,因为行为需要尽可能地模仿。此外,这需要更改用于测试的计时器类型。

int main()
{
    boost::asio::io_service io;

    // Internal timer set to expire in 24 hours.
    custom_timer t(io, boost::posix_time::hours(24));

    // Store user handler into user_handler_.
    t.async_wait(&print);

    io.poll(); // Nothing should happen - no handlers ready

    // Modify expiry time.  The internal timer's handler will be ready to
    // run with an error of operation_aborted.
    t.expires_from_now(t.expires_from_now() - boost::posix_time::hours(24));

    // The internal handler will be called, and handle the case where the
    // expiry time changed to timeout.  Thus, print will be called with
    // success.
    io.poll();

    return 0;
}

创建自定义WaitableTimerService

创建自定义WaitableTimerService有点复杂。虽然文档说明了API以及前/后条件,但实现需要了解一些内部结构,例如io_service实现和调度程序接口,它通常是一个反应堆。如果服务将用户的处理程序传递给调度程序,则在到期时间改变时将通知用户处理程序。因此,与包装计时器类似,必须在内部管理用户处理程序。

这与包装计时器有相同的缺点:需要更改类型,并且在尝试匹配前/后条件时由于潜在错误而继承风险。

例如:

deadline_timer timer;

相当于:

basic_deadline_timer<boost::posix_time::ptime> timer;

并将成为:

basic_deadline_timer<boost::posix_time::ptime,
                     boost::asio::time_traits<boost::posix_time::ptime>,
                     CustomTimerService> timer;

虽然可以使用typedef减轻这种情况:

typedef basic_deadline_timer<
  boost::posix_time::ptime,
  boost::asio::time_traits<boost::posix_time::ptime>,
  CustomTimerService> customer_timer;

创建自定义处理程序。

可以使用处理程序类来包装实际的处理程序,并提供与上面相同的方法,并具有额外的自由度。虽然这需要更改类型并修改提供给async_wait的内容,但它提供了灵活性,因为自定义处理程序的API没有预先存在的要求。这种降低的复杂性提供了最小的风险解决方案。

int main()
{
    boost::asio::io_service io;

    // Internal timer set to expire in 24 hours.
    deadline_timer t(io, boost::posix_time::hours(24));

    // Create the handler.
    expirable_handler handler(t, &print);
    t.async_wait(&handler);

    io.poll();  // Nothing should happen - no handlers ready

    // Cause the handler to be ready to run.
    // - Sets the timer's expiry time to negative infinity.
    // - The internal handler will be ready to run with an error of
    //   operation_aborted.
    handler.set_to_expire();

    // The internal handler will be called, and handle the case where the
    // expiry time changed to timeout.  Thus, print will be called with
    // success.
    io.poll();

    return 0;
}

总而言之,以传统方式测试异步程序可能非常困难。通过适当的封装,如果没有条件构建,甚至几乎不可能进行单元测试。有时,它有助于转换视角并将整个异步调用链视为一个单元,所有外部处理程序都是API。如果异步链很难测试,那么我经常发现该链很难理解和/或维护,并将其标记为重构的候选者。另外,我经常编写帮助程序类型,允许我的测试工具以同步方式处理异步操作。

答案 2 :(得分:1)

我不知道如何伪造时间过去等东西,我认为提供自己的时间服务是过度的。但这是一个想法:

通过使用硬编码的24h初始化计时器,您使用的东西可以被视为魔术常量(意思是:您不应该做什么)。相反,你可以试试这个:

boost::asio::deadline_timer t(io, getDeadLineForX());

现在,如果你在测试套件中存根getDeadLineForX函数,你可以通过一个足够小的截止日期来测试计时器,你不必等待24小时才能完成测试套件。

答案 3 :(得分:1)

SSCCE,基于@free_coffee发布的link

#include <boost/asio.hpp>
#include <boost/optional.hpp>

class mock_time_traits
{       
    typedef boost::asio::deadline_timer::traits_type  source_traits;

public:

    typedef source_traits::time_type time_type;
    typedef source_traits::duration_type duration_type;

    // Note this implemenation requires set_now(...) to be called before now()
    static time_type now() { return *now_; }

    // After modifying the clock, we need to sleep the thread to give the io_service
    // the opportunity to poll and notice the change in clock time
    static void set_now(time_type t) 
    { 
        now_ = t; 
        boost::this_thread::sleep_for(boost::chrono::milliseconds(2)); 
    }

    static time_type add(time_type t, duration_type d) { return source_traits::add(t, d); }
    static duration_type subtract(time_type t1, time_type t2) { return source_traits::subtract(t1, t2); }
    static bool less_than(time_type t1, time_type t2) { return source_traits::less_than(t1, t2); }

    // This function is called by asio to determine how often to check 
    // if the timer is ready to fire. By manipulating this function, we
    // can make sure asio detects changes to now_ in a timely fashion.
    static boost::posix_time::time_duration to_posix_duration(duration_type d) 
    { 
        return d < boost::posix_time::milliseconds(1) ? d : boost::posix_time::milliseconds(1);
    }

private:

    static boost::optional<time_type> now_;
};

boost::optional<mock_time_traits::time_type> mock_time_traits::now_;



typedef boost::asio::basic_deadline_timer<
            boost::posix_time::ptime, mock_time_traits> mock_deadline_timer;

void handler(const boost::system::error_code &ec)
{
    std::cout << "Handler!" << std::endl;
}


int main()
{
    mock_time_traits::set_now(boost::posix_time::time_from_string("2013-01-20 1:44:01.000"));

    boost::asio::io_service io_service;
    mock_deadline_timer timer(io_service, boost::posix_time::seconds(5));
    timer.async_wait(handler);

    std::cout << "Poll 1" << std::endl;
    io_service.poll();

    mock_time_traits::set_now(mock_time_traits::now() + boost::posix_time::seconds(6));


    std::cout << "Poll 2" << std::endl;
    io_service.poll();

    std::cout << "Poll 3" << std::endl;
    io_service.poll();

    return 0;
}

// Output
Poll 1
Poll 2
Handler!
Poll 3

感谢@free_coffee提供this link来自boost asio创建者的博客文章。以上略有修改(我相信略有改进)。通过不在系统时钟上使用偏移量,您可以完全控制定时器:在您明确将时间向前移动超过定时器之前,它们不会触发。

可以通过使this_thread::sleep部分可配置来改进解决方案。请注意,[1]中描述的to_posix_duration黑客需要使用比sleep更短的持续时间。

对我而言,这种方法似乎仍然有点神奇,因为time_traits没有很好地记录,特别是to_posix_duration的黑客对它有一种伏都教的气味。我想它只是归结为deadline_timer实现的深入了解(我没有)。