In blackbox testing, we can use diff command to check an output file against expected results. To check standard output during unit testing, we can use a mock stdout and stderr to capture any outputs.
Using unittest module
Sample programme
import sys
class HelloClass():
def __init__(self, name):
if name == '':
print('Warning: empty string!', file = sys.stderr)
def hello(self):
print('Hi, {}'.format(self.name))
def update_name(self):
new_name = input('Your new name: ')
self.name = new_name
print('You have changed your name!')
def main():
print('main() is called', file = sys.stderr)
if __name__ == '__main__':
main()
Expected Behaviours
- When the main() function is called, the message “main() is called” will be printed to stdout.
- When an empty string is passed to the HelloClass() contructor, the messsage “Warning: empty string!” will be printed to stderr.
- When HelloClass() is initialised, a name is set.
- When hello() is called, the message “Hi, <name>” is printed to standard output.
- When update_name() is called, a new name can be set by standard input. A message is printed if successful.
Test stdout
import unittest
from unittest.mock import patch
from io import StringIO
# Import our program
import hello_world as hw
class TestHelloClass(unittest.TestCase):
@patch('sys.stdout', new_callable = StringIO)
def test_main(self, stdout):
# Test main()
hw.main()
extected_out = 'main() is called\n'
# Check stdout
self.assertEqual(stdout.getvalue(), extected_out)
if __name__ == '__main__':
unittest.main()
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Run test with:
python -m unittest hello_world_unittest
Test stderr
import unittest
from unittest.mock import patch
from io import StringIO
import hello_world as hw
class TestHelloClass(unittest.TestCase):
# The order of decorator is important!
@patch('sys.stdout', new_callable = StringIO)
@patch('sys.stderr', new_callable = StringIO)
def test_constructor(self, stderr, stdout):
# Test empty string passed to HelloClass contructor
hc = hw.HelloClass('')
hc.hello()
# Expect a warning message to stderr
expected_err = 'Warning: empty string!\n'
# Expect an empty string used as name
expected_out = 'Hi, \n'
# Check stdout and stderr
self.assertEqual(stdout.getvalue(), expected_out)
self.assertEqual(stderr.getvalue(), expected_err)
if __name__ == '__main__':
unittest.main()
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Test stdin
To simulate user input to stdin, we can pass a string to io.StringIO() and mock sys.stdin.
import unittest
from unittest.mock import patch
from io import StringIO
import hello_world as hw
class TestHelloClass(unittest.TestCase):
# Note that no argument is passed to function after patching sys.stdin
@patch('sys.stdin', StringIO('Bob\n'))
@patch('sys.stdout', new_callable = StringIO)
def test_change_name(self, stdout):
# Initial class
hc = hw.HelloClass('John')
hc.hello() # Hi, John
hc.update_name() # stdin is read
hc.hello() # Hi, Bob
expected_out = """Hi, John
Your new name: You have changed your name!
Hi, Bob
"""
self.assertEqual(stdout.getvalue(), expected_out)
if __name__ == '__main__':
unittest.main()
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Using pytest
Install pytest
Assuming you have pip installed with your Python, installing pytest is simply running:
$ pip install -U pytest
Check your pytest version:
$ pytest --version
pytest 6.2.4
capsys and monkeypatch
pytest can capture all standard outputs/errors for you. Simply pass capsys into test functions and call capsys.readouterr() to access captured output.
monkeypatch is simular to unittest.patch. It can replace an object and simulate input/output. See the following sample test cases.
from io import StringIO
# Import our program
import hello_world as hw
def test_main(capsys):
# Test main()
hw.main()
expected_out = 'main() is called\n'
# Read captured outputs
captured = capsys.readouterr()
# Check stdout
assert captured.out == expected_out
def test_constructor(capsys):
# Test empty string passed to HelloClass contructor
hc = hw.HelloClass('')
hc.hello()
# Expect a warning message to stderr
expected_err = 'Warning: empty string!\n'
# Expect an empty string used as name
expected_out = 'Hi, \n'
# Read captured outputs
captured = capsys.readouterr()
# Check stdout and stderr
assert captured.out == expected_out
assert captured.err == expected_err
def test_change_name(capsys, monkeypatch):
monkeypatch.setattr('sys.stdin', StringIO('Bob\n'))
# Initial class
hc = hw.HelloClass('John')
hc.hello() # Hi, John
hc.update_name() # stdin is read
hc.hello() # Hi, Bob
expected_out = """Hi, John
Your new name: You have changed your name!
Hi, Bob
"""
# Read captured outputs
captured = capsys.readouterr()
# Check stdout and stderr
assert captured.out == expected_out
Run the tests with:
pytest hello_world_pytest.py
============================= test session starts =============================
platform win32 -- Python 3.8.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: C:\artifacts
collected 3 items
hello_world_pytest.py ... [100%]
============================== 3 passed in 0.01s ==============================
There are more options available to configure the stdout/stderr capturing with pytest.
Detailed documentation.