Testing schedule library python - Time and Events

3.5k Views Asked by At

Let's suppose I am using the schedule library on a Ubuntu remote server to trigger an event every 3 days.

Code should look something similar to this:

import schedule
import time

def job():
    print("I'm working...")

schedule.every(3).days.at("10:30").do(job)

while True:
    schedule.run_pending()
    time.sleep(1)

How can I speed up my clock or test this code?

2

There are 2 best solutions below

0
On BEST ANSWER

Instead of faking out the clock, you can test this code in a different way.

Obviously you can write a unit test for the job() function (i.e. make sure it is doing what it's supposed to). This is probably obvious to you. You're probably wondering how you can test that the main script will call your function properly, but you can't ensure it is 10:30 when you run your tests.

Enter the famous "monkey patch". Since Python has first class functions, you can simply bind names to functions. I'm not going to get into the unit test framework and how to use the mock library, but here's a quick example of what you might be looking for:

import schedule
import time

def mock_run_pending():
    job()

def mock_time_sleep(num):
    exit()

schedule.run_pending = mock_run_pending
time.sleep = mock_time_sleep

def job():
    print("I'm working...")

schedule.every(3).days.at("10:30").do(job)

while True:
    schedule.run_pending()
    time.sleep(1)

So what's going on here? If you run the code snippet, you'll notice that the job function is actually called! It is called exactly once, and then the program exits. The reason is, we are simply rebinding the names of the functions in the time and schedule modules to our "mock" versions (to make them more testable).

Edit: We can go even crazier and test that we are passing in the proper args to the scheduler (I couldn't help myself):

import schedule
import time

class MockEvery(object):
    def __init__(self, num):
        assert(num == 3)
        self.days = MockDays()

class MockDays(object):
    def __init__(self):
        pass

    def at(self, time):
        assert(time == "10:30")
        return MockAt()

class MockAt(object):
    def __init__(self):
        pass

    def do(self, func):
        assert(func == job)

def mock_every(num):
    return MockEvery(num)

def mock_run_pending():
    job()

def mock_time_sleep(num):
    exit()

schedule.run_pending = mock_run_pending
time.sleep = mock_time_sleep
schedule.every = mock_every

def job():
    print("I'm working...")

schedule.every(3).days.at("10:30").do(job)

while True:
    schedule.run_pending()
    time.sleep(1)

P.S. (this is somewhat irrelevant to your question, but you might find it useful): If you're looking to incorporate this into a testing framework, check out the mock package, and in particular, the @patch decorator. It allows you to monkey patch functions inside of a unit test (or any function for that matter). You'll also not want to call exit() if you have other tests. It's easy to exit out of a particular test with pytest, it's a bit harder to do with the built-in unittest, but certainly possible. But, I digress.

HTH.

1
On

For reference, it is also possible to just fake the clock using the freezegun library:

from datetime import datetime, timedelta

import schedule
import freezegun

def job():
    print("I'm working...")


now = datetime(2020, 1, 1, 10, 31)
with freezegun.freeze_time(now) as frozen_date:
    schedule.every(3).days.at("10:30").do(job)
    schedule.run_pending()  # nothing happens
    frozen_date.move_to(now + timedelta(days=3))
    schedule.run_pending()  # job has run