views:

91

answers:

3

Howdy!

I am currently writing a small application with Python (3.1), and like a good little boy, I am doctesting as I go. However, I've come across a method that I can't seem to doctest. It contains an input(), an because of that, I'm not entirely sure what to place in the "expecting" portion of the doctest.

Example code to illustrate my problem follows:

"""
>>> getFiveNums()
Howdy. Please enter five numbers, hit <enter> after each one
Please type in a number:
Please type in a number:
Please type in a number:
Please type in a number:
Please type in a number:
"""

import doctest

numbers = list()

# stores 5 user-entered numbers (strings, for now) in a list
def getFiveNums():
    print("Howdy. Please enter five numbers, hit <enter> after each one")
    for i in range(5):
        newNum = input("Please type in a number:")
        numbers.append(newNum)
    print("Here are your numbers: ", numbers)

if __name__ == "__main__":
    doctest.testmod(verbose=True)

When running the doctests, the program stops executing immediately after printing the "Expecting" section, waits for me to enter five numbers one after another (without prompts), and then continues. As shown below:

doctest results

I don't know what, if anything, I can place in the Expecting section of my doctest to be able to test a method that receives and then displays user input. So my question (finally) is, is this function doctestable?

+2  A: 

The simplest way to make this testable would be parameter injection:

def getFiveNums(input_func=input):
    print("Howdy. Please enter five numbers, hit <enter> after each one")
    for i in range(5):
        newNum = input_func("Please type in a number:")
        numbers.append(newNum)
    print("Here are your numbers: ", numbers)

You can't realistically be expected to unit test input/output like that -- you cannot be concerned that the call to input might somehow fail. Your best option is to pass in a stub method of some nature; something like

def fake_input(str):
    print(str)
    return 3

So that in your doctest, you actually test getFiveNums(fake_input).

Moreover, by breaking the direct dependency on input now, if you were to port this code to something else later that didn't use a command line, you could just drop in the new code to retrieve input (whether that would be a dialog box in a GUI application, or a Javascript popup in a web-based application, etc.).

Mark Rushakoff
+4  A: 

I know you are asking for a doctest answer but may I suggest that this type of function may not be a good candidate for doctest. I use doctests for documentation more than testing and the doctest for this wouldn't make good documentation IMHO.

A unitest approach may look like:

import unittest

# stores 5 user-entered numbers (strings, for now) in a list
def getFiveNums():
    numbers = []
    print "Howdy. Please enter five numbers, hit <enter> after each one"
    for i in range(5):
        newNum = input("Please type in a number:")
        numbers.append(newNum)
    return numbers

def mock_input(dummy_prompt):
    return 1

class TestGetFiveNums(unittest.TestCase):
    def setUp(self):
        self.saved_input = __builtins__.input
        __builtins__.input = mock_input

    def tearDown(self):
        __builtins__.input = self.saved_input

    def testGetFiveNums(self):
        printed_lines = getFiveNums()
        self.assertEquals(printed_lines, [1, 1, 1, 1, 1])

if __name__ == "__main__":
    unittest.main()

It's maybe not exactally testing the function you put forward but you get the idea.

Paul Hildebrandt
+1  A: 

I found a different way.

"""
>>> getFiveNums(testing=True)
Howdy. Please enter five numbers, hit <enter> after each one
Please type in a number: 1
Please type in a number: 1
Please type in a number: 1
Please type in a number: 1
Please type in a number: 1
Here are your numbers:  [1, 1, 1, 1, 1]
"""

import doctest

numbers = list()

# stores 5 user-entered numbers (strings, for now) in a list
def getFiveNums(testing=False):
    print("Howdy. Please enter five numbers, hit <enter> after each one")
    for i in range(5):
        newNum = input("Please type in a number: ")
        if testing:
            print newNum
        numbers.append(newNum)
    print "Here are your numbers: ", numbers

if __name__ == "__main__":
    doctest.testmod(verbose=True)

The changes I made to your code are.

  1. ...(testing=True)... --> first line doctest
  2. Here are your numbers: [1, 1, 1, 1, 1] --> final line doctest
  3. def getFiveNums(testing=False) --> def statement
  4. ... input("Please type in a number: ") --> I added a space after the colon
  5. if testing:\n print newNum --> This prints the numbers entered when is test mode. In normal mode the numbers will be printed as you type them. I is necessary for formatting purposes as well. Remove it an note what happens.
  6. print "Here are your numbers: ", numbers --> I changed the layout of the final print statement

Save the above code in a file called foo.py

Now make a file called input.txt. All it needs in it is.

1
1
1
1
1

Five ones. One on each line.

To test you program do the following, at terminal or command prompt (I'm using a mac):

$ python foo.py < input.txt

This is easily changeable for any kind of user input on any program. With this you can now copy the output of terminal session and use it as your doctest.

NOTE: the function call in terminal would be getFiveNums(). In you doctest it needs to be getFiveNums(testing=True).

Even though doctest doesn't appear to be intended to be used in this way it is still a handy hack.

Marlen T. B.