18

可能であれば、単体テストでブースト タイマーをトリガーする目的でどのように時間をモックしますか?

たとえば、次のようなことを達成することは可能ですか。

#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

4 に答える 4

8

basic_deadline_timerテンプレートには、独自のクロックを提供するために使用できるtraitsパラメータがあります。Boost Asio の作成者は、これを行う方法を示すブログ投稿を持っています。投稿の例を次に示します。

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

于 2013-01-20T02:20:47.017 に答える
4

私の知る限り、時刻の変更をエミュレートしたり、Boostで時刻を設定したりする方法はありません。この問題に対処するために使用できるいくつかの手法を詳しく説明する前に、考慮すべき点がいくつかあります。

  • Boost.Asio はクロックを使用するタイマーを提供しますが、Boost.Asio の範囲外であるためクロックを提供しません。したがって、設定やエミュレートなどのクロック関連の機能は、Boost.Asio の機能の範囲内ではありません。
  • モノトニック クロックは内部で使用できます。したがって、クロック (エミュレートまたは実際のクロック) を変更しても、目的の効果が得られない場合があります。たとえば、boost::asio::steady_timerはシステム時間の変更の影響を受けませんepoll。システム クロックの変更から保護されているため、reactor の実装はシステム時間の変更を検出するまでに最大 5 分かかる場合があります。
  • Boost.Asio タイマーの場合、有効期限を変更すると、WaitableTimerServiceおよびTimerServiceの要件に従って非同期待機操作が暗黙的にキャンセルされます。このキャンセルにより、未処理の非同期待機操作ができるだけ早く完了し、キャンセルされた操作のエラー コードは になりboost::asio::error::operation_abortedます。

それにもかかわらず、テスト対象に基づいて、この問題に対処するための 2 つの全体的な手法があります。

  • スケーリング時間。
  • ラッピングタイプ。

スケーリング時間

スケーリング時間は、複数のタイマー間で同じ全体的な相対フローを維持します。たとえば、有効期限が 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提供された をasync_wait()内部に格納します ( user_handler_)。
  • cancel()呼び出されると、キャンセルが発生したことを示す内部フラグが設定されます ( cancelled_)。
  • タイマーを集約します。有効期限が設定されると、内部ハンドラが集約されたタイマーの に渡されasync_waitます。内部ハンドラーが呼び出されるたびに、次の 4 つのケースを処理する必要があります。
    • 通常のタイムアウト。
    • 明示的なキャンセル。
    • 時間に変更された有効期限からの暗黙のキャンセルは、未来ではありません。
    • 有効期限が将来の時間に変更されることによる暗黙のキャンセル。

内部ハンドラ コードは次のようになります。

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の作成は、もう少し複雑です。io_serviceドキュメントには API と事前/事後条件が記載されていますが、実装には、実装や、多くの場合リアクタであるスケジューラ インターフェイスなど、内部の一部を理解する必要があります。サービスがユーザーのハンドラをスケジューラに渡すと、有効期限が変更されるとユーザー ハンドラに通知されます。したがって、タイマーをラップするのと同様に、ユーザー ハンドラーを内部で管理する必要があります。

これには、タイマーをラップするのと同じ欠点があります。型を変更する必要があり、事前/事後条件を一致させようとするとエラーが発生する可能性があるため、継承のリスクがあります。

例えば:

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 として、非同期呼び出しチェーン全体を 1 つの単位として扱うことが役立つ場合があります。非同期チェーンをテストするのが難しすぎる場合は、そのチェーンを理解および/または維持するのが難しすぎることに気づき、リファクタリングの候補としてマークします。さらに、多くの場合、テスト ハーネスが非同期操作を同期的に処理できるようにするヘルパー型を作成する必要があります。

于 2013-01-19T06:42:49.777 に答える
1

@free_coffee によって投稿されたリンクに基づくSSCCE:

#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

boost asio の作成者のブログ エントリへのリンクを提供してくれた @free_coffee に感謝します。上記はわずかに変更されています (そして、わずかに改善されていると思います)。システム クロックのオフセットを使用しないことで、タイマーを完全に制御できるようになります。タイマーは、タイマーを超えて時間を明示的に設定するまで起動しません。

このソリューションは、this_thread::sleepパーツを構成可能にすることで改善できます。[ 1 ]で説明されているto_posix_durationハックでは、 .sleep

私には、このアプローチはまだ少し魔法のように思えtime_traitsますto_posix_duration. それは、実装に関する詳細な知識に帰着するだけだと思いdeadline_timerます(私は持っていません)。

于 2013-01-20T03:59:06.013 に答える
1

時間の経過などを偽造する方法については知りません。独自の時間サービスを提供するのはやり過ぎだと思います。しかし、ここに考えがあります:

ハードコーディングされた 24 時間でタイマーを初期化することにより、魔法の定数と見なすことができる何かを使用しました (つまり、すべきでないこと)。代わりに、これを試すことができます:

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

ここで、テスト スイートで関数をスタブ化するgetDeadLineForXと、タイマーをテストするのに十分な短い期限を渡すことができ、テスト スイートが完了するまで 24 時間待つ必要はありません。

于 2013-01-11T14:12:31.840 に答える