10

I depend on hardware that may or may not respond. As a consequence I frequently end up writing functions with timeouts. System time is a known source for brittle unit tests so injecting a controlled and stable time seems like a good idea for testing.

I wonder if there are any facilities in std::chrono that help with that. The alternative I see is to write a wrapper around the system time and depend on that adapter.

Here is a minimal example of how a wrapper could look like.

#pragma once
#include <memory>
#include <chrono>
#include <thread>
#include <iostream>

using std::chrono::system_clock;
using std::chrono::milliseconds;
using std::shared_ptr;
using std::make_shared;

class Wrapped_Clock
{
public:
    virtual system_clock::time_point Now() { return system_clock::now(); }
    virtual void Sleep(milliseconds ms) { std::this_thread::sleep_for(ms); }
};

class Mock_Clock : public Wrapped_Clock
{
private:
    system_clock::time_point now;
public:
    Mock_Clock() : now(system_clock::now()){}
    ~Mock_Clock() {}
    system_clock::time_point Now() { return now; }
    void Sleep(milliseconds ms) { }
};

class CanTimeOut
{
private:
    shared_ptr<Wrapped_Clock> sclock;
public:
    CanTimeOut(shared_ptr<Wrapped_Clock> sclock = make_shared<Wrapped_Clock>()) : sclock(sclock) {}
    ~CanTimeOut() {}

    milliseconds TimeoutAction(milliseconds maxtime)
    {
        using std::chrono::duration_cast;
        int x = 0;
        system_clock::time_point start = sclock->Now();
        system_clock::time_point timeout = sclock->Now() + maxtime;
        while (timeout > sclock->Now() && x != 2000)
        {
            sclock->Sleep(milliseconds(1));
            ++x;
        }
        milliseconds elapsed = duration_cast<milliseconds>(sclock->Now() - start);
        return elapsed;
    }

};

#define EXPECT_GE(left, right, test) \
{ if (!(left >= right)) { \
    std::cout << #test << " " << "!(" << left << " >= " << right << ")" << std::endl; \
} }

#define EXPECT_EQ(expected, actual, test) \
{ if (!(expected == actual)) { \
    std::cout << #test << " " << "!(" << expected << " == " << actual << ")" << std::endl; \
} }

void TestWithSystemClock()
{
    CanTimeOut cto;
    long long timeout = 1000;
    milliseconds actual = cto.TimeoutAction(milliseconds(timeout));
    EXPECT_GE(actual.count(), timeout, TestWithSystemClock);
}

void TestWithMockClock()
{
    CanTimeOut cto(make_shared<Mock_Clock>());
    milliseconds actual = cto.TimeoutAction(milliseconds(1000));
    EXPECT_EQ(0, actual.count(), TestWithMockClock);
}

int main()
{
    TestWithSystemClock();
    TestWithMockClock();
}

How much of this can be replaced with functionality from std::chrone?

Edit 1:

  • "What exactly are you testing?" I am controlling time as a test condition to change the behaviour of method calls that depend on time. The Test illustrates that mocking the time and controlling behaviour as a concept works and shows my understanding of it. The point of the minimal example is to show my understanding of mocking time to make it easier to show the differences to the std:: facilities.
  • "spend ~10 words saying what the tests should contrast." The one Test always times out. The other Test shows no passage of time. A third Test that controls an exact and non zero passage of time was not included.
  • "Besides, sleep has nothing to do with the clock. It's not a chrono feature." I needed it to ensure that the one test never loops more than a certain amount before timing out, this simulates some action that takes time and can time out. On the other hand I wanted to build in a shortcut so the second test does not waste time waiting. It would be ok to not mock Sleep as well but the test would take 2 seconds. I recognize the point that Sleep is not a chrono feature and therefor misleading.
4

2 回答 2

8

相反,它看起来你在嘲笑std::this_thread::sleep.

这有点棘手,因为它是一个只有自由函数的命名空间。为了测试目的,很难“注入”一个命名空间。因此,您确实应该使用您自己的类型包装该命名空间中的函数。

我会使用静态依赖注入,比如 C++:

Live On Coliru

#include <memory>
#include <chrono>
#include <thread>
#include <iostream>

using std::chrono::system_clock;
using std::chrono::milliseconds;

struct production {
    using clock = std::chrono::system_clock;

    struct this_thread {
        template<typename... A> static auto sleep_for(A&&... a) { return std::this_thread::sleep_for(std::forward<A>(a)...); }
        template<typename... A> static auto sleep_until(A&&... a) { return std::this_thread::sleep_until(std::forward<A>(a)...); }
    };
};

struct mock {
    struct clock : std::chrono::system_clock {
        using base_type = std::chrono::system_clock;
        static time_point now() { static auto onetime = base_type::now(); return onetime; }
    };

    struct this_thread {
        template<typename... A> static auto sleep_for(A&&... a) {}
        template<typename... A> static auto sleep_until(A&&... a) {}
    };
};

template <typename services = production,
         typename clock = typename services::clock,
         typename this_thread = typename services::this_thread>
class CanTimeOut
{
public:
    milliseconds TimeoutAction(milliseconds maxtime)
    {
        using std::chrono::duration_cast;

        int x = 0;
        auto start   = clock::now();
        auto timeout = clock::now() + maxtime;
        while (timeout > clock::now() && x != 2000)
        {
            this_thread::sleep_for(milliseconds(1));
            ++x;
        }
        milliseconds elapsed = duration_cast<milliseconds>(clock::now() - start);
        return elapsed;
    }

};

#define EXPECT_GE(left, right, test) \
{ if (!(left >= right)) { \
    std::cout << #test << " " << "!(" << left << " >= " << right << ")" << std::endl; \
} }

#define EXPECT_EQ(expected, actual, test) \
{ if (!(expected == actual)) { \
    std::cout << #test << " " << "!(" << expected << " == " << actual << ")" << std::endl; \
} }

void TestWithSystemClock()
{
    CanTimeOut<> cto;
    long long timeout = 1000;
    milliseconds actual = cto.TimeoutAction(milliseconds(timeout));
    EXPECT_GE(actual.count(), timeout, TestWithSystemClock);
}

void TestWithMockClock()
{
    CanTimeOut<mock> cto;
    milliseconds actual = cto.TimeoutAction(milliseconds(1000));
    EXPECT_EQ(0, actual.count(), TestWithMockClock);
}

int main()
{
    TestWithSystemClock();
    TestWithMockClock();
}
于 2015-11-09T10:46:40.483 回答
1

处理此问题的另一种方法是定义模拟时钟并指定要用作模板参数的时钟类型。

#include <chrono>
#include <iostream>
#include <thread>

#include "sim_clock.hpp"

using namespace std::chrono;

template <typename clock_t> void Sleep(milliseconds ms)
{
    std::this_thread::sleep_for(ms);
}

template <> void Sleep<sim_clock>(milliseconds ms)
{
    sim_clock::increment_by(ms);
}

template <typename clock_t = std::chrono::steady_clock> class CanTimeOut
{
  public:
    CanTimeOut() = default;
    ~CanTimeOut() = default;

    milliseconds TimeoutAction(milliseconds maxtime)
    {
        int x = 0;
        auto start = clock_t::now();
        auto timeout = start + maxtime;
        while(timeout > clock_t::now()) { Sleep<clock_t>(milliseconds(1)); }
        return duration_cast<milliseconds>(clock_t::now() - start);
    }
};

#define EXPECT_GE(left, right, test)                                           \
    {                                                                          \
        if(!(left >= right)) {                                                 \
            std::cout << #test << " "                                          \
                      << "!(" << left << " >= " << right << ")" << std::endl;  \
        }                                                                      \
    }

void TestWithSystemClock()
{
    CanTimeOut<> cto;
    long long timeout = 1000;
    milliseconds actual = cto.TimeoutAction(milliseconds(timeout));
    EXPECT_GE(actual.count(), timeout, TestWithSystemClock);
}

void TestWithMockClock()
{
    CanTimeOut<sim_clock> cto;
    long long timeout = 1000;
    milliseconds actual = cto.TimeoutAction(milliseconds(timeout));
    sim_clock::increment_by(milliseconds(timeout));
    EXPECT_GE(actual.count(), timeout, TestWithSystemClock);
}

int main()
{
    TestWithSystemClock();
    TestWithMockClock();
}

这是基于的模拟时钟定义steady_clock

#pragma once
#include <chrono>

struct sim_clock {
    typedef std::chrono::steady_clock::rep rep;
    typedef std::chrono::steady_clock::period period;
    typedef std::chrono::steady_clock::duration duration;
    typedef std::chrono::steady_clock::time_point time_point;

    static time_point now() noexcept;
    static void increment_by(sim_clock::duration d) noexcept;

    static constexpr bool is_steady = true;

    static time_point _now;
};

和实施:

#include "sim_clock.hpp"

sim_clock::time_point sim_clock::_now;

sim_clock::time_point sim_clock::now() noexcept
{
    return _now;
}

void sim_clock::increment_by(sim_clock::duration d) noexcept
{
    _now += d;
}
于 2018-12-26T18:45:28.237 回答