Reputation: 4020
If you search Google or SO for 'unit test stdin stdout python' you will find very many questions, each and every one of which is answered in one way or another with
Do you really need to unit test Python's builtin
input
/sys.stdin
methods?
My answer is yes, I emphatically do, because I'm essentially implementing my own input
+ poor-man's libreadline
/ libcurses
, and I need to test using stdin and the contents of the terminal.
I happen to use a Unix-derived OS so I have pipes |
and shell redirection <
, so I could write a little shell script to do this alongside some Python helper code, and to test the terminal's actions (ANSI escape sequences, cursor movement, exactly what gets printed, etc) I could read from a known /dev/tty/whatever
, but there are two main reasons I don't want to do this:
Testing code should be as cross-platform as the code it's testing (and not so fragile!)
I quite like unittest
, thank you, and I don't want to resort to shell scripting and unix hackery (as much as I like unix hackery) just to test my module.
There must be a better way to test things like curses
, not when you're using curses
but when you're developing a curses
.
Since it was requested, here's some examples of what I'm looking to test: (full code on github)
def _Getch():
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
class xyctl:
def _terminal_size():
import fcntl, struct
h, w, hp, wp = struct.unpack('HHHH',
fcntl.ioctl(0, termios.TIOCGWINSZ,
struct.pack('HHHH', 0, 0, 0, 0)))
return w, h, w * h
def _matrix_calc(adj_x, adj_y):
cur_x, cur_y = xyctl.getter()
new_x, new_y = (
(cur_x + adj_x),
(cur_y + adj_y)
)
if (new_x * new_y) < (xyctl._terminal_size()[2]):
return new_x, new_y
else:
_writer(CHAR_BEL)
def getter():
_writer(CHAR_ESC + "[6n")
pos = until("R", raw=True)
_writer(CHAR_CRR + CHAR_SPC * (len(pos) + 1) + CHAR_CRR)
pos = pos[2:].split(";")
pos[0], pos[1] = int(pos[1]), int(pos[0])
return pos
def setter(new_x, new_y):
_writer(CHAR_ESC + "[{};{}H".format(new_x, new_y))
def adjust(adj_x, adj_y):
new_x, new_y = xyctl._matrix_calc(adj_x, adj_y)
xyctl.setter(new_x, new_y)
Upvotes: 2
Views: 592
Reputation: 1832
Late to the party here, but: I'm in exactly this situation, wanting to test the actual user-facing portions of the CLI, to be able to (i) ensure consistent behavior resulting from a given set of user keypresses, and (ii) inspect the actual content printed to console.
I put together a couple of classes that achieve this in (what seems to me) a pretty nice way, originally implemented here and, as of this writing, now published in v1.0 pre-release on PyPI as stdio-mgr. Most of it isn't especially novel, just a context manager to mock stdout
/stderr
/stdin
over to temporary streams, as already described in answers like this one, this one and this one.
The big difference from anything I've seen elsewhere is that stdin
is mocked to a custom StringIO
subclass which (a) automatically tees everything read from the stream over to the mocked stdout
, and thus echoes the "typed input" to the "console"; and (b) implements an .append()
helper method to allow addition of more content to the end of mock-stdin
without changing the seek position.
Example usage:
>>> from stdio_mgr import stdio_mgr
>>> with stdio_mgr() as (in_, out_, err_):
... print('foobar') # 'print' appends '\n'
... in_.append('bazquux\n') # Don't forget trailing '\n'!
... inp_result = input('?') # 'bazquux\n' is teed to 'out_' here
... out_result = out_.getvalue() # Pull the whole 'out_' stream contents
>>> inp_result
'bazquux'
>>> out_result
'foobar\n?bazquux\n'
Note that even though the string appended to in_
was newline-terminated (otherwise execution would hang), per the standard behavior input
stripped that trailing '\n'
before the string was stored in inp_result
. As can be seen, the question mark used as the prompt for input
is also stored in out_
.
However, since TeeStdin
tees the content read from "stdin
" before it's passed to input
, out_
received ?bazquux\n
, not ?bazquux
. While this newline handling has the potential to get somewhat confounded, I'm pretty sure there's not much to be done about it—any twiddles would probably break the transparency of the mock from the perspective of the wrapped code.
Upvotes: 1
Reputation: 3235
You can use the subprocess
module to call your script from within python. You can then send test input via communicate
import subprocess as sp
import sys
interpreter_path = sys.executable
p = sp.Popen([interpreter_path, script_to_test])
(stdout, stderr) = p.communicate(input = testinput)
stdout
and stderr
can then be tested for correct values
Upvotes: 2