Search code examples
pythonunit-testingpytestcs50

Writing tests for Python program


I have written a Python program that prompts users for letters A or B, then for a number to determine how long it will take them to accrue 1 000 000. I need help on how to write a test for the functions def profile, option_check(f) all using PYTEST.

import math
import sys
# Global variables

s = 1000000 # final amount
r = float(11.88/100) # convert s&p500 annual return to effective monthly rate
i = float(r / 12) # effective rate



def main():
    x = input('Do you want to:\n (A): calculate how long it will take to save your first million investing a fixed amount in the S&P500 monthly?\n OR \n (B): Calculate how much you should save in the S&P500 per month to reach 1 000 000 within your time period?\n Type A or B: ')

    letter = profile(x)
    result = option_check(letter)
    print(result)
    sys.exit()



 # do users want how long it will take or how much to save within a period
def profile(x):
    while True:
        try:
            letter = x.upper()
            if letter == 'A' or letter == 'B':
                return letter
            else:
                main()
        except ValueError:
            print('INVALID !  Please enter: A or B')

def option_check(f):
    if f == 'A':
        pay_periods, amount = option_a()
        years, months = time(pay_periods)
        result = f"It will take {years} years and {months} months investing {amount} in the S&P500 to reach 1000 000"
        return result
    if f == 'B':
        amount, time_frame = option_b()
        result = f'You will reach 1 000 000 in {time_frame} years by investing {amount} in the S&P500 Monthly'
        return result



def option_a():
    # error check for ints
    while True:
        try:
            # monthly amount
            p = int(input('How much do you plan on saving in the S&P500 per month?\n'))

            # calculate years
            top = 1 - (s * (1 - (1 + i)) / p)

            periods = math.log(top) / math.log(1 + i)

            return periods, p

        except:
            print('Enter valid number\n')

def time(x):
    years = math.floor(x / 12)
    months = math.floor(12 * (x / 12 - math.floor(years)))
    return years, months


def option_b():
    while True:
        try:
            time_frame = int(input('What time frame do you have to reached 1 000 000 in years?\n'))

            # calculationg p: the monthly payment needed
            periods = time_frame * 12
            top = s - s * (1 + i)
            bottom = 1 - (1 + i ) ** periods
            amount = round(top / bottom , 2)
            return amount, time_frame
        except:
            print('Please Enter VALID NUMBER...\n')



if __name__ == '__main__':
    main()
from forecast import option_check, time, profile

def main():
    test_time()
    test_option_check()


def test_time():
    assert time(12) == 1

def test_option_check():
    assert option_check('?')

def test_profile():
    assert profile('A')

if __name__ == '__main__':
    main()

Since the functions return more than 1 value how would I test for this?


Solution

  • What you are trying to accomplish is unit testing the code. This usually means that certain parts of the code will need to be patched or mocked, and your case is no exception.

    First, lets look at the forecast.py file. I have added some inline comments and changed the code around a little.

    forecast.py

    import math
    import sys
    # Global variables
    
    s = 1000000  # final amount
    r = float(11.88/100)  # convert s&p500 annual return to effective monthly rate
    i = float(r / 12)  # effective rate
    
    
    def main():
        x = input('Do you want to:\n (A): calculate how long it will take to save your first million investing a fixed amount in the S&P500 monthly?\n OR \n (B): Calculate how much you should save in the S&P500 per month to reach 1 000 000 within your time period?\n Type A or B: ')
    
        letter = profile(x)
        result = option_check(letter)
        print(result)
        sys.exit()
    
    
    # do users want how long it will take or how much to save within a period
    def profile(x):
        try:
            letter = x.upper()
            # This if else will never raise a ValueError because if its not A or B the else just re-runs main()
            # To raise a ValueError if A or B not given, you need to raise the error within the else
            # The while True will also cause an infinite error loop, so remove that
            if letter == 'A' or letter == 'B':
                return letter
            else:
                raise ValueError()
        except ValueError:
            print('INVALID !  Please enter: A or B')
    
    
    def option_check(f):
        if f == 'A':
            pay_periods, amount = option_a()
            years, months = time(pay_periods)
            result = f"It will take {years} years and {months} months investing {amount} in the S&P500 to reach 1000 000"
            return result
        if f == 'B':
            amount, time_frame = option_b()
            result = f'You will reach 1 000 000 in {time_frame} years by investing {amount} in the S&P500 Monthly'
            return result
    
    
    def option_a():
        # error check for ints
        while True:
            try:
                # monthly amount
                p = int(input('How much do you plan on saving in the S&P500 per month?\n'))
    
                # calculate years
                top = 1 - (s * (1 - (1 + i)) / p)
    
                periods = math.log(top) / math.log(1 + i)
    
                return periods, p
            # Try to be specific with error handling when possible
            except ValueError:
                print('Enter valid number\n')
    
    
    def time(x):
        years = math.floor(x / 12)
        months = math.floor(12 * (x / 12 - math.floor(years)))
        return years, months
    
    
    def option_b():
        while True:
            try:
                time_frame = int(input('What time frame do you have to reached 1 000 000 in years?\n'))
    
                # calculating p: the monthly payment needed
                periods = time_frame * 12
                top = s - s * (1 + i)
                bottom = 1 - (1 + i) ** periods
                amount = round(top / bottom, 2)
                return amount, time_frame
            # Try to be specific with error handling when possible
            except ValueError:
                print('Please Enter VALID NUMBER...\n')
    
    
    if __name__ == '__main__':
        main()
    

    Now lets look at the test file:

    test.py

    import pytest
    from forecast import option_check, time, profile
    from unittest.mock import patch
    
    
    def test_time():
        # Since time() returns a tuple we can assign a var to each member of the returned tuple
        # This will allow us to assert individually on those vars for the correct answers
        years, months = time(12)
        assert years == 1
        assert months == 0
    
    
    # The mark parametrize will allow us to test multiple different inputs in one test function
    @pytest.mark.parametrize("option", ["A", "B"])
    def test_option_check(option):
        # Need to patch the input message for testing purposes
        # Then we can run the code with the return_value of 1 as the user input
        with patch("builtins.input", return_value=1):
            result = option_check(option)
            # This assertion allows us to assert on the correct message
            # given the option we passed in from the parametrize decorator
            assert result == ("It will take 77 years and 9 months investing 1 in the S&P500 to reach 1000 000" if option == "A" else "You will reach 1 000 000 in 1 years by investing 78892.66 in the S&P500 Monthly")
    
    
    @pytest.mark.parametrize("letter", ["A", "a", "B", "b"])
    def test_profile(letter):
        if letter in ["A", "a"]:
            result = profile(letter)
            assert result == "A"
        elif letter in ["B", "b"]:
            result = profile(letter)
            assert result == "B"
    

    The main part to pay attention to here is the pytest.mark.parametrize and the patch().

    Links: parametrize: https://docs.pytest.org/en/7.1.x/example/parametrize.html patch: https://docs.python.org/3/library/unittest.mock.html

    Both the links above will help explain what these do. The general idea though is:

    Parametrize --> This allows us to build parameters for a given test function which will parametrize the test function with the given parameters. Each parameter will build a new instance of that test function allowing us to test many parameters without having to duplicate the test function.

    Mock (specifically Patch()) --> This allows us to patch an instance of the code with a return value or side effect. There is a lot to these libraries so I suggest looking into the link provided.