Table of Content
Unit testing our code in python is very essential in everyday development.
However, we sometimes find it difficult to test inputs from the shell prompts.
This gets even worse when dealing with passwords.
In this post, you will learn how to effectively handle such cases when next you encounter them:
In this tutorial, we will use the click module.
Let's define a function that handles prompts for both a username and a password, like below:
Create a test login function
import click
def login():
"""Prompt for and collecting user login details."""
username = click.prompt("Username")
password = click.prompt("Password", hide_input=True)
return username, password
Unit test for the login function
We can write a test using mock data:
from unittest import TestCase, mock
@contextmanager
def input(*cmds):
"""Replace input."""
cmds = "\n".join(cmds)
with mock.patch("sys.stdin", StringIO(f"{cmds}\n"))):
yield
class TestInput(TestCase):
"""Test the prompt input."""
def test_login(self):
"""Test returning the user login data collected from prompt."""
with input("dummy.username", "admin1234"):
self.assertEqual(login(), ("dummy.username", "admin1234"))
This is okay.
When you create a mock for the standard input's return values, values are required to be passed together as a string stream, with each command separated by a newline character, representing the “Enter” key.
If we run the test,
The problem
The test routine hangs at the password input, indicating that we are missing something in our mocking logic.
If you look beyond its documentation for the click module by digging deeper in its code, you will see that it handles password prompts differently for regular visible inputs.
Hence, hidden inputs are implemented using Python's built-in getpass
module, which omits echoing the typed-in characters.
It also acts as a convenient abstraction layer across both the Unix and Windows operating systems.
To customize this for what we want, we need to introduce a new logic branch to tell visible inputs from hidden ones in our mock in the getpass
module.
The solution
We will return the hidden values as callable side_effect's and pass a different data type for hidden inputs with some kind of placeholder key.
Let's tweak our mock function and test accordingly:
from unittest import TestCase, mock
@contextmanager
def input(*cmds):
"""Replace input."""
visible_cmds = "\n".join([c for c in cmds if isinstance(c, str)])
hidden_cmds = [c.get("hidden") for c in cmds if isinstance(c, dict)]
with mock.patch("sys.stdin", StringIO(f"{visible_cmds}\n")),
mock.patch("getpass.getpass", side_effect=hidden_cmds):
yield
class TestInput(TestCase):
"""Test the prompt input."""
def test_login(self):
"""Test returning the user login data from prompt."""
with input("dummy.username", {"hidden" : "admin1234"}):
self.assertEqual(collect_user_data(), ("dummy.username", "admin1234"))
Run the test again:
You will get a success message indicating that the test passed.
The solution above allows you to mock as many subsequent prompts as you want, regardless of whether they are visible or hidden, and in whichever order you arrange them.
Wrap Off
And there you have it.
This is how you can effectively test for visible and non-visible values in the terminal/command prompt shell inputs using the Python click module.
If you learned from this tutorial, or it helped you in any way, please consider sharing and subscribing to our newsletter.