19

If possible, how do you mock the time for the purpose of triggering boost timers in a unit test?

For example, is it possible to achieve something like the following:

#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;
}

Update Thank you to all the answers, they have provided excellent insight into the problem. I have provided my own answer (SSCCE), but couldn't have done that without the help provided.

Zero
  • 11,593
  • 9
  • 52
  • 70

4 Answers4

9

The basic_deadline_timer template has a traits parameter which you can use to provide your own clock. The author of Boost Asio has a blog post showing how to do this. Here's an example from the 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;

Maybe you can use something like offset_timer throughout your application but only call set_now() when running your tests?

free_coffee
  • 396
  • 2
  • 3
  • Excellent reference. This is exactly what I was looking for, and has the benefit of being really simple. As an aside, I wouldn't use that timer everywhere, I simply inject a 'real' or 'mock' timer depending whether I'm running tests or in production. – Zero Jan 20 '13 at 03:30
4

As far as I know, there is no way to emulate time change or to set the time with Boost. Before expanding upon a few techniques that can be used to approach this problem, there are a few points to consider:

  • Boost.Asio provides timers that use clocks, but does not provide clocks as they are outside of the scope of Boost.Asio. Thus, clock related features, such as setting or emulating, are not within Boost.Asio's capabilities.
  • Monotonic clocks may be used internally. Thus, a change in a clock (emulated or actual) may not produce the desired effect. For example, boost::asio::steady_timer will not be affected by changes to system time, and the reactor implementation using epoll can take up to 5 minutes before detecting changes to system time, as it is protected from changes to the system clock.
  • For Boost.Asio timers, changing the expiration time will implicitly cancel asynchronous wait operations per the WaitableTimerService and TimerService requirements. This cancellation causes outstanding asynchronous wait operations to complete as soon as possible, and cancelled operations will have an error code of boost::asio::error::operation_aborted.

Nevertheless, there are two overall techniques to approach this problem based on what is being tested:

  • Scaling time.
  • Wrapping types.

Scaling Time

Scaling time preserves the same overall relative flow between multiple timers. For example, a timer with a 1 second expiration should trigger before a timer with a 24 hour expiration. Minimum and maximum durations can also be used for additional control. Furthermore, scaling durations works for timers that are not affected by the system clock, as as the steady_timer.

Here is an example, where a scale of 1 hour = 1 second is applied. Thus, the 24 hour expiration will actual be a 24 second expiration. Additionally,

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();
}

Wrapping types

There are a few distinct locations where new types can be introduced to obtain some of the desired behavior.

In all of these cases, it is important to account for the behavior that changing the expiration time will implicitly cancel the asynchronous wait operation.

Wrap the deadline_timer.

Wrapping the deadline_timer requires managing the user's handler internally. If the timer passes the user's handler to the service associated with the timer, then the user handler will be notified when the expiry time changes.

A custom timer could:

  • Store the WaitHandler provided to async_wait() internally (user_handler_).
  • When cancel() is invoked, an internal flag is set to indicate that cancellation has occurred (cancelled_).
  • Aggregate a timer. When an expiry time is set, an internal handler is passed to the aggregated timer's async_wait. Anytime the internal handler is called, it needs to handle the following four cases:
    • A normal timeout.
    • An explicit cancellation.
    • An implicit cancellation from expiry time being changed to a time is not in the future.
    • An implicit cancellation from expiry time being changed to a time that is in the future.

The internal handler code may look like the following:

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));
  }
}

While this is fairly easy to implement, it requires understanding the timer interface enough to mimic its pre/post-conditions, with the exception of the behavior for which you want to deviate. There may also be a risk factor in testing, as the behaviors need to be mimicked as close as possible. Additionally, this requires changing the type of timer for testing.

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;
}

Create a custom WaitableTimerService

Creating a custom WaitableTimerService is a little bit more complex. Although the documentation states the API, and the pre/post conditions, the implementation requires an understanding some of the internals, such as the io_service implementation and the scheduler interface, which is often a reactor. If the service passes the user's handler to the scheduler, then the user handler will be notified when the expiry time changes. Thus, similar to wrapping a timer, the user handler must be managed internally.

This has the same drawbacks as wrapping a timer: requires changing types and has inherit risk due to potential errors when trying to match the pre/post conditions.

For example:

deadline_timer timer;

is the equivalent of:

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

and would become:

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

Although this could be mitigated with a typedef:

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

Create a custom handler.

A handler class could be used to wrap the actual handler, and provide the same approach as above with an extra degree of freedom. While this requires changing a type, and modifying what is provided to async_wait, it provides flexibility in that the custom handler's API has no pre-existing requirements. This reduced complexity provides a minimal risk solution.

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;
}

All in all, testing asynchronous programs in a traditional manner can be very difficult. With proper encapsulation, it may even be nearly impossible to unit test without conditional builds. Sometimes it helps to shift perspectives and treat the entire asynchronous call chain as a single unit, with all external handlers being the API. If an asynchronous chain is too difficult to test, then I often find that the chain is too difficult to understand and/or maintain, and will mark it as a candidate for refactoring. Additionally, I often have to write helper types that allow my test harness to treat the asynchronous operations in a synchronous manner.

Tanner Sansbury
  • 51,153
  • 9
  • 112
  • 169
  • Excellent overview, and I can see each approach being useful in different circumstances. From my perspective, the best approach would be creating a custom handler, that has access to a global clock. In production this would be the system clock, and in test a test clock. That way, all timers in the system will still behave in sync, just responding to a different clock. Also, this approach means that handlers are still being called from the `io.poll()` and not directly from tests, I think is preferable (but not crucial). This last point is what makes this approach better than wrapping. – Zero Jan 20 '13 at 01:02
  • For anyone else trying to write a custom handler the best resource I've come across is http://en.highscore.de/cpp/boost/index.html – Zero Jan 20 '13 at 01:03
  • Lastly: typo in my above comment, I meant 'custom WaitableTimerService'. – Zero Jan 20 '13 at 01:43
1

I dont' know about how to fake something like time passing, and I consider it to be overkill to provide your own time service. But here's a thought:

By initializing the timer with a hardcoded 24h, you used something that could be considered a magic constant (meaning: what you should not do). Instead, you could try this:

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

Now, if you stub out the getDeadLineForX function in your test suite, you can pass a sufficiently small deadline to test the timer, and you don't have to wait 24 hours for your test suite to complete.

Arne Mertz
  • 24,171
  • 3
  • 51
  • 90
  • That's not a bad idea as a workaround (I do something similar), but it becomes unworkable when you have many timers and complex logic (ie the timers are reset depending on what events occur). – Zero Jan 13 '13 at 23:07
  • Fake a timer is super useful for unit test, I think is something that should be mentioned in the Boost documentation – Elvis Dukaj Apr 04 '20 at 09:25
1

A SSCCE, based on a link posted by @free_coffee:

#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

Thankyou to @free_coffee for providing this link to a blog entry from the creator of boost asio. The above is slightly modified (and I believe slightly improved). By not using an offset on the system clock, you gain complete control over the timers: they will not fire until you explicitly set time forward past the timer.

The solution could be improved by making the this_thread::sleep part configurable. Note that the to_posix_duration hack described in [1] needs to use a smaller duration than the sleep.

To me this approach still seems a bit magic, since the time_traits are not well documented, and in particular the hack of to_posix_duration has a whiff of voodoo about it. I guess it just comes down to intimate knowledge of the deadline_timer implementation (which I don't have).

Zero
  • 11,593
  • 9
  • 52
  • 70