如果可能的话,你如何模拟在单元测试中触发升压定时器的时间?
例如,是否可以实现以下内容:
#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),但没有提供的帮助就无法做到。
答案 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。在扩展可用于解决此问题的一些技术之前,需要考虑以下几点:
epoll
的反应器实现可能需要长达5分钟才能检测到系统时间的更改,因为它可以防止系统更改时钟。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
。包装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
实现的深入了解(我没有)。