how to measure sleep duration: test_duration_w_date_and_time


This is IV of a program that calculates the difference between a given wake and sleep time.


I want to test the duration function with timestamps that have dates

RED: make it fail

  • I make a copy of random_timestamp, change the name of the copy, then add a date to the return statement

    def random_timestamp_a():
        return (
            '1999/12/31 '
            f'{random.randint(0,23):02}:'
            f'{random.randint(0,59):02}'
        )
    
  • I also make a copy of test_duration_w_hours_and_minutes and change the name to test_duration_w_date_and_time

  • then add calls to random_timestamp_a

    def test_duration_w_date_and_time(self):
        sleep_time = random_timestamp_a()
        wake_time = random_timestamp_a()
    
        while wake_time < sleep_time:
            self.assertWakeTimeEarlier(
                wake_time, sleep_time
            )
            wake_time = random_timestamp_a()
        else:
            self.assertEqual(
                src.sleep_duration.duration(
                    sleep_time=sleep_time,
                    wake_time=wake_time
                ),
                self.get_difference(
                    wake_time=wake_time,
                    sleep_time=sleep_time
                )
            )
    

    the terminal is my friend, and shows ValueError

    ValueError: invalid literal for int() with base 10: '1999/12/31 03'
    ValueError: invalid literal for int() with base 10: '1999/12/31 07'
    ValueError: invalid literal for int() with base 10: '1999/12/31 11'
    ValueError: invalid literal for int() with base 10: '1999/12/31 21'
    

    the test calls duration, which calls read_timestamp, which uses int() to change the timestamp string to a number after it calls str.split, and it is not in the right format

GREEN: make it pass

  • I add the `unittest.skip decorator`_ to test_duration_w_date_and_time

    @unittest.skip
    def test_duration_w_date_and_time(self):
    ...
    
  • then add an assertion in test_converting_strings_to_numbers to see if I can make the same ValueError happen again to make sure the problem is with converting the string to a number

    def test_converting_strings_to_numbers(self):
        self.assertEqual(int('12'), 12)
        self.assertEqual(int('01'), 1)
    
        int('1999/12/31 21')
    

    the terminal is my friend, and shows ValueError with the same message from test_duration_w_date_and_time

    ValueError: invalid literal for int() with base 10: '1999/12/31 21'
    

    I cannot use int() to change a timestamp string to a number when it has a date. I add an assertRaises to handle the ValueError

    def test_converting_strings_to_numbers(self):
        self.assertEqual(int('12'), 12)
        self.assertEqual(int('01'), 1)
    
        with self.assertRaises(ValueError):
            int('1999/12/31 21')
    

    and the test is green again

  • I remove the `unittest.skip decorator`_ from test_duration_w_date_and_time

  • then change the call in the assertion to a different function

    def test_duration_w_date_and_time(self):
        sleep_time = random_timestamp_a()
        wake_time = random_timestamp_a()
    
        while wake_time < sleep_time:
            self.assertWakeTimeEarlier(
                wake_time=wake_time,
                sleep_time=sleep_time
            )
            wake_time = random_timestamp_a()
        else:
            self.assertEqual(
                src.sleep_duration.duration_a(
                    sleep_time=sleep_time,
                    wake_time=wake_time
                ),
                self.get_difference(
                    wake_time=wake_time,
                    sleep_time=sleep_time
                )
            )
    

    the terminal is my friend, and shows AttributeError

    AttributeError: module 'src.sleep_duration' has no attribute 'duration_a'...
    
  • I make a copy of the duration function in sleep_duration.py and change the name to duration_a to keep the working solution while I try a new one. I change the else block to return None

    def duration_a(wake_time=None, sleep_time=None):
        if wake_time < sleep_time:
            raise ValueError(
                f'wake_time: "{wake_time}"'
                ' is earlier than '
                f'sleep_time: "{sleep_time}"'
            )
        else:
            return None
    

    the terminal is my friend, and shows ValueError

    ValueError: invalid literal for int() with base 10: '1999/12/31 22'
    

    because the test calls the get_difference method in the expectation which uses int()

  • I change it to return wake_time and sleep_time

    def test_duration_w_date_and_time(self):
        sleep_time = random_timestamp_a()
        wake_time = random_timestamp_a()
    
        while wake_time < sleep_time:
            self.assertWakeTimeEarlier(
                sleep_time=sleep_time,
                wake_time=wake_time,
            )
            wake_time = random_timestamp_a()
        else:
            self.assertEqual(
                src.sleep_duration.duration_a(
                    wake_time=wake_time,
                    sleep_time=sleep_time
                ),
                (wake_time, sleep_time)
            )
    

    the terminal is my friend, and shows AssertionError

    AssertionError: None != ('1999/12/31 09:52', '1999/12/31 07:11')
    AssertionError: None != ('1999/12/31 18:16', '1999/12/31 11:21')
    AssertionError: None != ('1999/12/31 13:10', '1999/12/31 12:00')
    AssertionError: None != ('1999/12/31 16:41', '1999/12/31 12:35')
    
  • I change the return statement in duration_a

    def duration_a(wake_time=None, sleep_time=None):
        if wake_time < sleep_time:
            raise ValueError(
                f'wake_time: "{wake_time}"'
                ' is earlier than '
                f'sleep_time: "{sleep_time}"'
            )
        else:
            return (wake_time, sleep_time)
    

    the test passes.

REFACTOR: make it better

I want something that can read dates and times from timestamps. I search Python’s online documentation for date and time to see if there is an existing solution and select the `datetime module`_ from the results. The available types in the module show `datetime.datetime`_ objects

class datetime.datetime
  A combination of a date and a time.
    Attributes: year, month, day, hour,
    minute, second, microsecond, and tzinfo.

test_datetime_objects

RED: make it fail

I add a test to test_sleep_duration.py from Examples of usage: datetime for `datetime.datetime`_ objects

def test_datetime_objects(self):
    self.assertEqual(
        datetime.strptime(
            "21/11/06 16:30",
            "%d/%m/%y %H:%M"
        ),
        ''
    )

def test_duration_w_date_and_time(self):
...

the terminal is my friend, and shows NameError

NameError: name 'datetime' is not defined. Did you forget to import 'datetime'

GREEN: make it pass

I add an import statement for the datetime_ module

import datetime
import random
import src.sleep_duration
import unittest
...

the terminal is my friend, and shows AttributeError

AttributeError: module 'datetime' has no attribute 'strptime'

because my import statement is different from the example in the documentation, so I add the module name to the call

def test_datetime_objects(self):
    self.assertEqual(
        datetime.datetime.strptime(
            "21/11/06 16:30",
            "%d/%m/%y %H:%M"
        ),
        ''
    )

the terminal is my friend, and shows AssertionError

AssertionError: datetime.datetime(2006, 11, 21, 16, 30) != ''

I copy the value from the left side of the AssertionError and paste it as the expected value in the test

def test_datetime_objects(self):
    self.assertEqual(
        datetime.datetime.strptime(
            "21/11/06 16:30",
            "%d/%m/%y %H:%M"
        ),
        datetime.datetime(
            2006, 11, 21, 16, 30
        )
    )

and it passes

The `datetime.datetime.strptime`_ method returns a `datetime.datetime`_ object, if given 2 strings as inputs - a timestamp and a pattern that is for the timestamp. The pattern provided is

  • %d for days

  • %m for months

  • %y for 2 digit years

  • %H for hours

  • %M for minutes

there are more details in strftime() and strptime() behavior

REFACTOR: make it better

  • I change the order in the date to test the pattern

    def test_datetime_objects(self):
        self.assertEqual(
            datetime.datetime.strptime(
                "06/11/21 16:30",
                "%d/%m/%y %H:%M"
            ),
            datetime.datetime(
                2006, 11, 21, 16, 30
            )
        )
    

    the terminal is my friend, and shows AssertionError

    AssertionError: datetime.datetime(2021, 11, 6, 16, 30) != datetime.datetime(2006, 11, 21, 16, 30)
    

    when I change the pattern to match

    def test_datetime_objects(self):
        self.assertEqual(
            datetime.datetime.strptime(
                "06/11/21 16:30",
                "%y/%m/%d %H:%M"
            ),
            datetime.datetime(
                2006, 11, 21, 16, 30
            )
        )
    

    the test passes.

  • I change the year to four digits

    def test_datetime_objects(self):
        self.assertEqual(
            datetime.datetime.strptime(
                "2006/11/21 16:30",
                "%y/%m/%d %H:%M"
            ),
            datetime.datetime(
                2006, 11, 21, 16, 30
            )
        )
    

    the terminal is my friend, and shows ValueError

    ValueError: time data '2006/11/21 16:30' does not match format '%y/%m/%d %H:%M'
    

    and when I change the pattern to use %Y for the year

    def test_datetime_objects(self):
        self.assertEqual(
            datetime.datetime.strptime(
                "2006/11/21 16:30",
                "%Y/%m/%d %H:%M"
            ),
            datetime.datetime(
                2006, 11, 21, 16, 30
            )
        )
    

    the test is green again

  • I add calls to the `datetime.datetime.strptime`_ method in test_duration_w_date_and_time

    def test_duration_w_date_and_time(self):
        sleep_time = random_timestamp_a()
        wake_time = random_timestamp_a()
    
        while wake_time < sleep_time:
            self.assertWakeTimeEarlier(
                wake_time=wake_time,
                sleep_time=sleep_time
            )
            wake_time = random_timestamp_a()
        else:
            self.assertEqual(
                src.sleep_duration.duration_a(
                    sleep_time=sleep_time,
                    wake_time=wake_time
                ),
                (
                    datetime.datetime.strptime(
                        wake_time,
                        '%Y/%m/%d %H:%M'
                    ),
                    datetime.datetime.strptime(
                        sleep_time,
                        '%Y/%m/%d %H:%M'
                    )
                )
            )
    

    the terminal is my friend, and shows AssertionError

    AssertionError: Tuples differ: ('1999/12/31 07:20', '1999/12/31 03:08') != (datetime.datetime(1999, 12, 31, 7, 20), datetime.datetime(1999, 12, 31, 3, 8))
    AssertionError: Tuples differ: ('1999/12/31 15:01', '1999/12/31 00:37') != (datetime.datetime(1999, 12, 31, 15, 1), datetime.datetime(1999, 12, 31, 0, 37))
    AssertionError: Tuples differ: ('1999/12/31 20:50', '1999/12/31 14:22') != (datetime.datetime(1999, 12, 31, 20, 50), [35 chars] 22))
    AssertionError: Tuples differ: ('1999/12/31 16:40', '1999/12/31 13:39') != (datetime.datetime(1999, 12, 31, 16, 40), [35 chars] 39))
    

    duration_a returns the timestamps as strings and the assertion expects them as `datetime.datetime`_ objects. I change the return statement to match

    def duration_a(wake_time=None, sleep_time=None):
        if wake_time < sleep_time:
            raise ValueError(
                f'wake_time: "{wake_time}"'
                ' is earlier than '
                f'sleep_time: "{sleep_time}"'
            )
        else:
            return (
                datetime.datetime.strptime(
                    wake_time, '%Y/%m/%d %H:%M'
                ),
                datetime.datetime.strptime(
                    sleep_time, '%Y/%m/%d %H:%M'
                )
            )
    

    the terminal is my friend, and shows NameError

    NameError: name 'datetime' is not defined. Did you forget to import 'datetime'
    

    I add an import statement to the top of sleep_duration.py

    import datetime
    
    
    def read_timestamp(timestamp=None, index=0):
    ...
    

    the test passes.

  • I just called `datetime.datetime.strptime`_ 5 times in a row with the same pattern, time to add a function to remove some repetition

    def get_datetime(timestamp):
        return datetime.datetime.strptime(
            timestamp, '%Y/%m/%d %H:%M'
        )
    
    
    def duration_a(wake_time=None, sleep_time=None):
    ...
    

    and call it in the return statement of duration_a

    def duration_a(wake_time=None, sleep_time=None):
        if wake_time < sleep_time:
            raise ValueError(
                f'wake_time: "{wake_time}"'
                ' is earlier than '
                f'sleep_time: "{sleep_time}"'
            )
        else:
            return (
                get_datetime(wake_time),
                get_datetime(sleep_time)
            )
    

    still green!

test_get_datetime

RED: make it fail

I want a test for the get_datetime function so I change the name of test_datetime_objects to test_get_datetime and make it call src.sleep_duration.get_datetime which calls the `datetime.datetime.strptime`_ method

def test_get_datetime(self):
    self.assertEqual(
        src.sleep_duration.get_datetime(
            "21/11/06 16:30"
        ),
        datetime.datetime(
            2006, 11, 21, 16, 30
        )
    )

I change the expectation to use `datetime.datetime.strptime`_

def test_get_datetime(self):
    self.assertEqual(
        src.sleep_duration.get_datetime(
            "2006/11/21 16:30"
        ),
        datetime.datetime.strptime(
            '2006/11/21 16:30',
            '%Y/%m/%d %H:%M'
        )
    )

then add a variable for a random timestamp

def test_get_datetime(self):
    timestamp = random_timestamp_a()
    self.assertEqual(
        src.sleep_duration.get_datetime(
            timestamp
        ),
        datetime.datetime.strptime(
            '2006/11/21 16:30',
            '%Y/%m/%d %H:%M'
        )
    )

the terminal is my friend, and shows AssertionError

AssertionError: datetime.datetime(1999, 12, 31, 0, 23) != datetime.datetime(2006, 11, 21, 16, 30)
AssertionError: datetime.datetime(1999, 12, 31, 8, 55) != datetime.datetime(2006, 11, 21, 16, 30)
AssertionError: datetime.datetime(1999, 12, 31, 9, 16) != datetime.datetime(2006, 11, 21, 16, 30)
AssertionError: datetime.datetime(1999, 12, 31, 15, 5) != datetime.datetime(2006, 11, 21, 16, 30)

GREEN: make it pass

I add the variable to the expectation

def test_get_datetime(self):
    timestamp = random_timestamp_a()
    self.assertEqual(
        src.sleep_duration.get_datetime(
            timestamp
        ),
        datetime.datetime.strptime(
            timestamp,
            '%Y/%m/%d %H:%M'
        )
    )

the test is green again

REFACTOR: make it better

I change the calls to `datetime.datetime.strptime`_ to src.sleep_duration.get_datetime

def test_duration_w_date_and_time(self):
    sleep_time = random_timestamp_a()
    wake_time = random_timestamp_a()

    while wake_time < sleep_time:
        self.assertWakeTimeEarlier(
            wake_time=wake_time,
            sleep_time=sleep_time
        )
        wake_time = random_timestamp_a()
    else:
        self.assertEqual(
            src.sleep_duration.duration_a(
                sleep_time=sleep_time,
                wake_time=wake_time
            ),
            (
                src.sleep_duration.get_datetime(
                    wake_time
                ),
                src.sleep_duration.get_datetime(
                    sleep_time
                )
            )
        )

and the test is still green.


  • I change the expectation in the test to the difference between the timestamps because I can do arithmetic with `datetime.datetime`_ objects

    def test_duration_w_date_and_time(self):
        sleep_time = random_timestamp_a()
        wake_time = random_timestamp_a()
    
        while wake_time < sleep_time:
            self.assertWakeTimeEarlier(
                wake_time=wake_time,
                sleep_time=sleep_time
            )
            wake_time = random_timestamp_a()
        else:
            self.assertEqual(
                src.sleep_duration.duration_a(
                    sleep_time=sleep_time,
                    wake_time=wake_time
                ),
                (
                    src.sleep_duration.get_datetime(
                        wake_time
                    )
                  - src.sleep_duration.get_datetime(
                        sleep_time
                    )
                )
            )
    

    the terminal is my friend, and shows AssertionError

    AssertionError: (datetime.datetime(1999, 12, 31, 16, 1), datetime.datetime(1999, 12, 31, 14, 6)) != datetime.timedelta(seconds=6900)
    AssertionError: (datetime.datetime(1999, 12, 31, 9, 57), datetime.datetime(1999, 12, 31, 1, 3)) != datetime.timedelta(seconds=32040)
    AssertionError: (datetime.datetime(1999, 12, 31, 23, 59),[35 chars], 1)) != datetime.timedelta(seconds=7080)
    AssertionError: (datetime.datetime(1999, 12, 31, 16, 1), [35 chars] 55)) != datetime.timedelta(seconds=7560)
    

    the duration_a function returns `datetime.datetime`_ objects and the assertion expects a `datetime.timedelta`_ object. I change it to match the expectation

    def duration_a(wake_time=None, sleep_time=None):
        if wake_time < sleep_time:
            raise ValueError(
                f'wake_time: "{wake_time}"'
                ' is earlier than '
                f'sleep_time: "{sleep_time}"'
            )
        else:
            return (
                get_datetime(wake_time)
              - get_datetime(sleep_time)
            )
    

    the test passes.

  • I add str() to the expectation in the test because I want the result as a string not a `datetime.timedelta`_ object

    def test_duration_w_date_and_time(self):
        sleep_time = random_timestamp_a()
        wake_time = random_timestamp_a()
    
        while wake_time < sleep_time:
            self.assertWakeTimeEarlier(
                wake_time=wake_time,
                sleep_time=sleep_time
            )
            wake_time = random_timestamp_a()
        else:
            self.assertEqual(
                src.sleep_duration.duration_a(
                    sleep_time=sleep_time,
                    wake_time=wake_time
                ),
                str(
                    src.sleep_duration.get_datetime(
                        wake_time
                    )
                  - src.sleep_duration.get_datetime(
                        sleep_time
                    )
                )
            )
    

    the terminal is my friend, and shows AssertionError

    AssertionError: datetime.timedelta(seconds=4440) != '1:14:00'
    AssertionError: datetime.timedelta(seconds=15780) != '4:23:00'
    AssertionError: datetime.timedelta(seconds=31020) != '8:37:00'
    AssertionError: datetime.timedelta(seconds=49920) != '13:52:00'
    

    when I make the same change to the return statement in duration_a

    def duration_a(wake_time=None, sleep_time=None):
        if wake_time < sleep_time:
            raise ValueError(
                f'wake_time: "{wake_time}"'
                ' is earlier than '
                f'sleep_time: "{sleep_time}"'
            )
        else:
            return str(
                get_datetime(wake_time)
              - get_datetime(sleep_time)
            )
    

    the test passes.

  • I remove duration because duration_a is a better solution

  • which means I can remove read_timestamp because no one calls it anymore. the terminal is my friend, and shows AttributeError

    AttributeError: module 'src.sleep_duration' has no attribute 'duration'...
    
  • I change the name of duration_a to duration in sleep_duration.py and test_sleep_duration.py which leaves me with ValueError

    ValueError: time data '04:51' does not match format '%Y/%m/%d %H:%M'
    ValueError: time data '13:35' does not match format '%Y/%m/%d %H:%M'
    ValueError: time data '12:26' does not match format '%Y/%m/%d %H:%M'
    ValueError: time data '23:20' does not match format '%Y/%m/%d %H:%M'
    

    test_duration_w_hours_and_minutes does not have dates in its timestamps. I remove it because it is covered by test_duration_w_date_and_time

  • then remove get_difference because no one calls it anymore

  • I also remove these tests because they do not test the solution directly

    • test_the_modulo_operation

    • test_floor_aka_integer_division

    • test_converting_strings_to_numbers

    • test_string_splitting

  • and remove random_timestamp because no one calls it anymore

  • then change the name of random_timestamp_a to random_timestamp

  • the new random_timestamp function always returns timestamps with the same date, I change it to return random dates as well

    def random_timestamp(date):
        return (
            f'{random.randint(0,9999):04}/'
            f'{random.randint(1,12):02}/'
            f'{random.randint(1,31):02} '
            f'{random.randint(0,23):02}:'
            f'{random.randint(0,59):02}'
        )
    

    the terminal is my friend, and shows a random ValueError

    ValueError: day is out of range for month
    

    I need to make sure the random dates that are made by the function are real dates

  • I change the name of random_timestamp to get_random_timestamp and make a new function to make sure the random timestamps generated are good

    def get_random_timestamp():
        return (
            f'{random.randint(0,9999):04}/'
            f'{random.randint(1,12):02}/'
            f'{random.randint(1,31):02} '
            f'{random.randint(0,23):02}:'
            f'{random.randint(0,59):02}'
        )
    
    
    def random_timestamp():
        result = get_random_timestamp()
        try:
            src.sleep_duration.get_datetime(result)
        except ValueError:
            return random_timestamp()
        else:
            return result
    

    The new random_timestamp function does these things

    • generates a random timestamp by calling get_random_timestamp

    • checks if the timestamp is good by calling src.sleep_duration.get_datetime

    • if the timestamp is good, the function returns it

    • if the timestamp is bad, it raises ValueError and repeats the process by calling itself

  • I can add another function to remove some repetition

    def random_number(start, end, digits=2):
        return f'{random.randint(start, end):0{digits}}'
    

    then add calls to it in get_random_timestamp

    def get_random_timestamp():
        return (
            f'{random_number(0,9999,4)}/'
            f'{random_number(1,12)}/'
            f'{random_number(1,31)} '
            f'{random_number(0,23)}:'
            f'{random_number(0,59)}'
        )
    

    all tests are still green.

  • I change test_duration_w_date_and_time to test_duration the terminal is my friend, and shows all tests are still passing

review

The challenge was to write a program that calculates the difference between a given wake and sleep time. I ran these tests to get something that does it

I also saw these Exceptions

Would you like to write the solution without looking at test_sleep_duration.py?


how to measure sleep duration: tests and solution