Добавить в корзинуПозвонить
Найти в Дзене

Тестируем код зависящий от временных интервалов

Представим что Вам нужно протестировать функцию, которая работает со временем, ожидая, к примеру, 5 секунд. Как протестировать такой код в google mock чтобы при каждом прогоне теста не ждать 5 секунд? Для того, чтобы тесты выполнялись мгновенно, необходимо вынести функционал работы с системным временем в отдельную абстракцию. Вместо прямого вызова std::this_thread::sleep_for или использования std::chrono::system_clock код должен обращаться к интерфейсу, абстрагирующему работу со временем. class IClock { public: virtual ~IClock() = default; virtual void sleep(std::chrono::milliseconds duration) = 0; }; В production коде используем унаследованный от IClock класс реально ожидающий заданный временной интервал. class RealClock : public IClock { public: void sleep(std::chrono::milliseconds duration) override { std::this_thread::sleep_for(duration); } Теперь возьмем следующий код и протестируем метод doWork. class TimerWorker { std::shared_ptr<IClock> clock_; public:

Представим что Вам нужно протестировать функцию, которая работает со временем, ожидая, к примеру, 5 секунд. Как протестировать такой код в google mock чтобы при каждом прогоне теста не ждать 5 секунд?

Для того, чтобы тесты выполнялись мгновенно, необходимо вынести функционал работы с системным временем в отдельную абстракцию. Вместо прямого вызова std::this_thread::sleep_for или использования std::chrono::system_clock код должен обращаться к интерфейсу, абстрагирующему работу со временем.

class IClock {

public:

virtual ~IClock() = default;

virtual void sleep(std::chrono::milliseconds duration) = 0;

};

В production коде используем унаследованный от IClock класс реально ожидающий заданный временной интервал.

class RealClock : public IClock {

public:

void sleep(std::chrono::milliseconds duration) override {

std::this_thread::sleep_for(duration);

}

Теперь возьмем следующий код и протестируем метод doWork.

class TimerWorker {

std::shared_ptr<IClock> clock_;

public:

TimerWorker(std::shared_ptr<IClock> clock) : clock_{clock} {}

void doWork() {

// Логика до...

clock_->sleep(std::chrono::seconds(5)); // Ждем 5 секунд

// Логика после...

}};

Создадим мок

class MockClock : public IClock {

public:

MOCK_METHOD(void, sleep, (std::chrono::milliseconds duration), (override));

};

и используем его в тесте:

TEST(TimerWorkerTest, DoesNotActuallyWait) {
auto mock_clock = std::make_shared<MockClock>();

TimerWorker worker(mock_clock);

// Ожидаем, что метод sleep будет вызван именно с аргументом 5000 мс

EXPECT_CALL(*mock_clock, sleep(std::chrono::milliseconds(5000))).Times(1);

// Выполняем метод. Тест пройдет мгновенно.

worker.doWork();

}

Этот подход имеет следующие преимущества:

  • Скорость. Экономия 5 секунд на каждом прогоне.
  • Проверка параметров. Вы точно знаете, что функция ожидает именно 5 секунд, а не 5 миллисекунд из-за опечатки.
  • Детерминизм. Тесты не зависят от нагрузки на процессор или планировщика задач ОС.

Для тестирования логики, которая зависит от интервалов времени (например, проверка, прошло ли 5 минут с последнего действия), расширим интерфейс IClock методом now().

class IClock {

public:

virtual ~IClock() = default;

virtual void sleep(std::chrono::milliseconds duration) = 0;

virtual system_clock::time_point now() const = 0;

};

Главная хитрость в тесте будет состоять в том, что мы вручную «перематываем» время вперед, возвращая разные значения при последовательных вызовах.

class MockClock : public IClock {

public:

MOCK_METHOD(void, sleep, (std::chrono::milliseconds duration), (override));

MOCK_METHOD(system_clock::time_point, now, (), (const, override));

};

Теперь напишем функционал.

class ActionProtector {

std::shared_ptr<IClock> clock_;

system_clock::time_point last_action_time_;

public:

ActionProtector(std::shared_ptr<IClock> clock) : clock_{clock} {}

bool canPerformAction() {

auto currentTime = clock_->now();

if ((currentTime - last_action_time_) >= seconds(5)) {

last_action_time_ = currentTime;

return true;

}

return false;

}};

Нам необходимо протестировать метод canPerformAction.

TEST(ActionProtectorTest, LimitsActionsByTime) {

auto mock_clock = std::make_shared<MockClock>();

ActionProtector protector(mock_clock);

auto start_time = system_clock::time_point(seconds(100));

auto under_limit = start_time + seconds(2);

auto over_limit = start_time + seconds(6);

// Настраиваем последовательность ответов:

// 1. Первый запрос - разрешаем

// 2. Второй запрос (через 2 сек) - должен запретить

// 3. Третий запрос (через 6 сек) - снова разрешаем

EXPECT_CALL(*mock_clock, now()).WillOnce(Return(start_time)) // Вызов 1

.WillOnce(Return(under_limit)) // Вызов 2

.WillOnce(Return(over_limit)); // Вызов 3

// 1. Первое действие разрешено

EXPECT_TRUE(protector.canPerformAction());

// 2. Второе действие СРАЗУ ЖЕ (хотя по логике прошло 2 сек) - ЗАПРЕЩЕНО

EXPECT_FALSE(protector.canPerformAction());

// 3. Третье действие (по логике прошло 6 сек) - РАЗРЕШЕНО

EXPECT_TRUE(protector.canPerformAction());

}

Таким образом, возвращая при помощи мока разные значения, можно протестировать условие if ((currentTime - last_action_time_) >= seconds(5)) . При этом тесты будут проходить быстро и не ждать 5 секунд.