[Tutor] how to unittest cli input
Cameron Simpson
cs at zip.com.au
Sun Oct 11 23:52:59 CEST 2015
On 11Oct2015 09:29, Alex Kleider <akleider at sonic.net> wrote:
>On 2015-10-10 18:10, Cameron Simpson wrote:
> However, you'r eusing input(), which unconditionally uses stdin and
> stdout. In that circumstance I'd consider this:
>[... temporarily replace stdin and stdout with test data ...]
>
>Yes indeed, and thank you for your input.
>Here's where I'm going with your suggestion:
>[...]
>test_data = 'test_src.txt'
>
>def data_collection_wrapper(collect, source=None):
> """
> """
> if source:
Minor remark: I would write "if src is not None:". In principle the empty
string is also "falsey" like None, making your plain "if src:" slightly
unreliable. Be precise!
> ostdin = sys.stdin
> ostdout = sys.stdout
> src = open(source, 'r')
> sys.stdin = src
> out = open('/dev/null', 'w') # Dump the prompts.
> sys.stdout = out
>
> ret = collect()
>
> if source:
> src.close()
> out.close()
> sys.stdin = ostdin
> sys.stdout = ostdout
>
> return ret
>
>
>def collect_data():
> ret = {}
> ret['first'] = input("Enter your first name: ")
> ret['last'] = input("Enter your last name: ")
> ret['phone'] = input("Your mobile phone #: ")
> return ret
That looks like what I had in mind.
If you expect to do this with several functions you could write a context
manager to push new values for stdin and stdout, call a function and restore.
The "contextlib" stdlib module provides a convenient way to write trivial
context managers using the "@contextmanager" decorator, which wraps a generator
function which does the before/after steps. Have a read. I'd be inclined to
write something like this (untested):
import sys
from contextlib import contextmanager
@contextmanager
def temp_stdinout(src, dst):
ostdin = sys.stdin
ostdout = sys.stdout
sys.stdin = src
sys.stdout = dst
yield None
sys.stdin = ostdin
sys.stdout = ostdout
and then in your test code:
with open(source) as src:
with open('/dev/null', 'w') as dst:
with temp_stdinout(src, dst):
ret = collect()
This has several benefits. Primarily, a context manager's "after" code _always_
runs, even if an exception is raise in the inner section. This means that the
files are always closed, and the old stdin and stdout always restored. This is
very useful.
You'll notice also that an open file is a context manager which can be used
with the "with" statement: it always closes the file.
You also asked (off list) what I meant by parameterisation. I mean that some of
your difficult stems from "collect_data" unconditionally using stdin and
stdout< and that you can make it more flexible by supplying the input and
output as paramaters to the function. Example (not finished):
def collect_data(src, dst):
ret = {}
ret['first'] = input("Enter your first name: ")
ret['last'] = input("Enter your last name: ")
ret['phone'] = input("Your mobile phone #: ")
return ret
Now, the input() builtin always uses stdin and stdout, but it is not hard to
write your own:
def prompt_for(prompt, src, dst):
dst.write(prompt)
dst.flush()
return src.readline()
and use it in collect_data:
def collect_data(src, dst):
ret = {}
ret['first'] = prompt_for("Enter your first name: ", src, dst)
ret['last'] = prompt_for("Enter your last name: ", src, dst)
ret['phone'] = prompt_for("Your mobile phone #: ", src, dst)
return ret
You can also make src and dst optional, falling back to stdin and stdout:
def collect_data(src=None, dst=None):
if src is None:
src = sys.stdin
if dst is None:
dst = sys.stdout
ret = {}
ret['first'] = prompt_for("Enter your first name: ", src, dst)
ret['last'] = prompt_for("Enter your last name: ", src, dst)
ret['phone'] = prompt_for("Your mobile phone #: ", src, dst)
return ret
Personally I would resist that in this case because the last thing you really
want in a function is for it to silently latch onto your input/output if you
forget to call it with all its arguments/parameters. Default are better for
things that do not have side effects.
>def main():
> print(collect_data()) # < check that user input works
> # then check that can test can be automated >
> print(data_collection_wrapper(collect_data,
> src=test_data))
>
>if __name__ == "__main__":
> main()
>
>Perhaps data_collection_wrapper could be made into a decorator (about
>which I am still pretty naive.)
>
>It'll take more studying on my part before I'll be able to implement
>Ben's suggestion.
>
>Alex
>ps I was tempted to change the "Subject:" to remove 'cli' and replace
>it with 'interactive' but remember many admonitions to not do that so
>have left it as is.
Best to leave these things as they are unless the topic totally changes. Then I
tend to adjust the Subject: to be:
Subject: very different topic (was: old topic)
presuming that it is still the same discussion.
Cheers,
Cameron Simpson <cs at zip.com.au>
More information about the Tutor
mailing list