How to Unit Test Terminal (Bash) Inputs in Python

Learn how to effectively handle invisible inputs like passwords on the shell prompt

Picture of Nsikak Imoh, author of Macsika Blog
Abstract image with the text How to Unit Test Terminal (Bash) Inputs in Python
Abstract image with the text How to Unit Test Terminal (Bash) Inputs in Python

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
    
Highlighted code sample.

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"))
    
Highlighted code sample.

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,

$ python3 -m unittest
    Username: Password:
    
Highlighted code sample.

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"))
    
Highlighted code sample.

Run the test again:

$ python3 -m unittest 
    Enter your desired username: Enter your desired password:.
    ----------------------------------------------------------------------
    Ran 1 test in 0.003s
    
    OK
    
Highlighted code sample.

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.

Get the Complete Code of Python Code Snippets on Github.

Connect with me.

Need an engineer on your team to grease an idea, build a great product, grow a business or just sip tea and share a laugh?