[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