jeopardyfan98
jeopardyfan98

Reputation: 13

Pytest not looping while testing with monkeypatching

I am very new to programming and I'm writing some code that collects information from the user and adds it to a spreadsheet. Here's the basis of what I'm trying to test:

# let's say this file is called collect.py

def infoBreakdown():
    userData = input("Enter a detail about your info: ")
    # add detail to a new column
    
    assignLetter = input("Want to assign a letter to this detail? (y/n) ")
    
    addLetter = False
    if assignLetter == "y":
        addLetter = True
    while addLetter:
        newLetter = input("Enter a letter to assign to this detail: ")
        # add letter to a single cell within that column
        # each additional letter adds to the same cell

        newLetter = input("Want to assign another letter for this detail?: ")
        if newLetter == "n":
            addLetter = False


def main():
    
    infoBreakdown()

    addInfo = True
    while addInfo:
        newInfo = input("Cool! Want to add more info? (y/n) ")
        if newInfo == "y":
            infoBreakdown()
        else:
            addInfo = False

main()

So I want my test to be able to loop through adding multiple details and letters, but for some reason it's only running through the while loops once (I know because I added a couple flags to make sure). Something even more confusing is that it only acknowledges the second runs and not the first. Here's my test code:

def input_main(prompt):
    inputRequest = {
        "Enter a detail about your info: ": "Detail #1",
        "Want to assign a letter to this detail? (y/n) ":, "y",
        "Enter a letter to assign to this detail: ":, "A",
        "Want to assign another letter for this detail? (y/n) ":, "y",
        "Enter a letter to assign to this detail: ":, "B",
        "Want to assign another letter for this detail? (y/n) ":, "n",
        "Cool! Want to add more info? (y/n) ": "y",
        "Enter a detail about your info: ": "Detail #2",
        "Want to assign a letter to this detail? (y/n) ":, "n",
        "Cool! Want to add more info? (y/n) ": "n"
}

    return inputRequest[prompt]

def test_main(monkeypatch):

    monkeypatch.setattr('builtins.input', input_main)

    assert collect.main() == None

So I'm expecting to see both "Detail #1" (and a cell with "AB") and "Detail #2" added to the spreadsheet, but it only has "Detail #2" on there when I run the test. This tells me it's probably not an issue of overwriting on the spreadsheet because they are input into different columns.

If I only run it for the first detail then I'd expect to see "Detail #1" and a cell with "AB" on the spreadsheet, but it only has "Detail #1" and a cell with "B".

This only happens when using pytest - if I test manually, the code is running fine. The problem is I ask for a lot more input than this and it's a huge waste of time to test manually. Any insight into what I'm missing?

Upvotes: 1

Views: 436

Answers (1)

thisisalsomypassword
thisisalsomypassword

Reputation: 1611

First of all: cudos for being new at programming and starting with writing tests.

The reason your test is not working the way you expect it to is that a dict does not work the way you try to use it. A dict is a mapping of unique keys to arbitrary values. While your keys can't, your values can contain duplicates. If you call print(len(InputRequest)), you will see that it only contains five entries:

{'Cool! Want to add more info? (y/n) ': 'n',
 'Enter a detail about your info: ': 'Detail #2',
 'Enter a letter to assign to this detail: ': 'B',
 'Want to assign a letter to this detail? (y/n) ': 'n',
 'Want to assign another letter for this detail? (y/n) ': 'n'}

If you write a dict-literal with duplicate keys, the duplicate keys will just map to the last value you assigned to them. This is why you only see "Detail #2" in your output. Your test only runs through the loops once because your last entries are supposed to finish the test.

What I do when I write tests that emulate user input, I use a Generator enclosed in a closure:

def make_mock_input(generator):
    def mock_input(*args, **kwargs): # ignore any arguments
        return next(generator) # the state of the generator is stored in the enclosing scope of make_mock_input

    return mock_input


def test_main(monkeypatch):
    def input_generator(user_input):
        yield from user_input

    user_input = ("Detail #1", "y", "A", "y", "B", "n", "y", "Detail #2", "n", "n")
    gen = input_generator(user_input) # create the generator from your input values
    mock_input = make_mock_input(gen)

    monkeypatch.setattr("builtins.input", mock_input)

    assert collect.main() is None # do identity checks with 'is', add checks for output

This way your mocked input function will return the next value in your sequence every time it is called (ignoring all passed arguments), just like a user would input their next value.

I feel obliged to say, while your test should work now, it is not a great test. Because the idea of a test is to raise an AssertionError if something is not working as expected. Right now you test will pretty much always pass, because collect.main() will always return None if there is no exception raised within it. You should add more assert statements, to programatically check the validity of your output. But I'm sure you will find loads of resources about writing good tests.

Upvotes: 1

Related Questions