How to supply stdin, files and environment variable inputs to Python unit tests?
Asked Answered
E

4

34

How to write tests where conditions like the following arise:

  1. Test user Input.
  2. Test input read from a file.
  3. Test input read from an environment variable.

It'd be great if someone could show me how to approach the above mentioned scenarios; it'd still be awesome if you could point me to a few docs/articles/blog posts which I could read.

Extern answered 11/4, 2010 at 12:57 Comment(3)
checking if I understand your question correctly: what you want to know is: how to test if an executable/script, given some inputs such as stdin/environment variables gives the expected outputs, such as stdout, stderr or exit status?Maricamarice
@CiroSantilli新疆改造中心六四事件法轮功 I exactly need that, do you know how to do it?Bruxelles
@JohnBalvinArias see my answer :-) https://mcmap.net/q/271036/-how-to-supply-stdin-files-and-environment-variable-inputs-to-python-unit-testsMaricamarice
S
34

All three situations you've described are where you need to specifically go out of your way to ensure you are using loose coupling in your design.

Do you really need to unit test Python's raw_input method? The open method? os.environ.get? No.

You need to set up your design so that you can substitute other ways of retrieving that input. Then, during your unit tests, you'll throw in a stub of some sort that doesn't actually call raw_input or open.

For instance, your normal code might be something like:

import os
def say_hello(input_func):
    name = input_func()
    return "Hello " + name

def prompt_for_name():
    return raw_input("What is your name? ")

print say_hello(prompt_for_name)
# Normally would pass in methods, but lambdas can be used for brevity
print say_hello(lambda: open("a.txt").readline())
print say_hello(lambda: os.environ.get("USER"))

The session looks like:

What is your name? somebody
Hello somebody
Hello [some text]

Hello mark

Then your test will be like:

def test_say_hello():
    output = say_hello(lambda: "test")
    assert(output == "Hello test")

Keep in mind that you should not have to test a language's IO facilities (unless you're the one designing the language, which is a different situation entirely).

Siobhan answered 11/4, 2010 at 13:18 Comment(3)
+1: This not only makes the code more testable, but also more reusable since it is not tied to a particular input source.Spiritoso
Except when you don't have the above under your control. The nice route is often unavailable.Occupational
So how does this approach work with multiple inputs assigned to multiple variables. Do you create multiple input_funcs or is there a more elegant way?Heliotaxis
R
30

If you are tied to using raw_input (or any other specific input source), I'm a big proponent of the mock library. Given the code that Mark Rushakoff used in his example:

def say_hello():
    name = raw_input("What is your name? ")
    return "Hello " + name

Your test code could use mock:

import mock

def test_say_hello():
     with mock.patch('__builtin__.raw_input', return_value='dbw'):
         assert say_hello() == 'Hello dbw'

     with mock.patch('__builtin__.raw_input', side_effect=['dbw', 'uki']):
         assert say_hello() == 'Hello dbw'
         assert say_hello() == 'Hello uki'

These assertions would pass. Note that side_effect returns the elements of the list in order. It can do so much more! I'd recommend checking out the documentation.

Rattray answered 27/6, 2013 at 22:1 Comment(3)
For Python 3, note that __builtin__ was renamed as builtins and raw_input() became input(), thus: mock.patch('builtins.input', return_value='dew')Sibelle
Mock is part of core python in Python 3 as from unittest import mock.Rattray
Thanks I like this approach (with the Python 3 variation) the best. Plus of course mocks are very helpful when using PytestNeurocoele
M
6

If you can get away without using an external process, do so.

However, there are situations where this is complicated, and you really want to use process, e.g., you want to test the command line interface of a C executable.

User input

Use subprocess.Popen as in:

process = subprocess.Popen(
    command,
    shell  = False,
    stdin  = subprocess.PIPE,
    stdout = subprocess.PIPE,
    stderr = subprocess.PIPE,
    universal_newlines = True
)
stdout, stderr = process.communicate("the user input\nline 2")
exit_status = process.wait()

There is no difference between taking input from a user and taking it from a pipe for input done with methods like raw_input or sys.stdin.read().

Files

  • Create a temporary directory and create the files you want to read from in there on your test setUp methods:

    tdir = tempfile.mkdtemp(
        prefix = 'filetest_', 
    )
    fpath = os.path.join(tdir,'filename')
    fp = open(fpath, 'w')
    fp.write("contents")
    fp.close()
    
  • Do the file reading in the tests.

  • Remove the temp dir afterwards.

    shutil.rmtree(tdir)
    
  • It is quite complicated reading from files, and most programs can read either from files or from STDIN (e.g. with fileinput). So, if what you want to test is what happens when a certain content is input, and your program accepts STDIN, just use Popen to test the program.

Environment variables

  • Set the environment variables with os.environ["THE_VAR"] = "the_val"
  • Unset them with del os.environ["THE_VAR"]
  • os.environ = {'a':'b'} does not work
  • Then call subprocess.Popen. The environment is inherited from the calling process.

Template Code

I have a module on my github that tests STDOUT, STDERR and the exit status given STDIN, command line arguments and enviroment. Also, check the tests for that module under the "tests" dir. There must be much better modules out there for this, so take mine just for learning purposes.

Maricamarice answered 8/4, 2013 at 8:14 Comment(2)
as I understand that only allows to insert the input once, what if there is a "while loop" that collects input from user n times?Bruxelles
@JohnBalvinArias unless there is timing involved, it is not possible to distinguish pipes from user input. If there is timing, have a look at pexpect.Maricamarice
O
3

Using pytest:

import os


def test_user_input(monkeypatch):
    inputs = [10, 'y']
    input_generator = (i for i in inputs)
    monkeypatch.setattr('__builtin__.raw_input', lambda prompt: next(input_generator))
    assert raw_input('how many?') == 10
    assert raw_input('you sure?') == 'y'


def test_file_input(tmpdir):
    fixture = tmpdir.join('fixture.txt')
    fixture.write(os.linesep.join(['1', '2', '3']))
    fixture_path = str(fixture.realpath())
    with open(fixture_path) as f:
        assert f.readline() == '1' + os.linesep


def test_environment_input(monkeypatch):
    monkeypatch.setenv('STAGING', 1)
    assert os.environ['STAGING'] == '1'
Occupational answered 23/4, 2015 at 8:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.