How to Test Python Input Loops

In this article, I will show you how to test the input loop of a command line interface app.

How to Test Python Input Loops

In this article, I will show you how to test the input loop of a command line interface app.

For the project, we will build a simple little calculator class. But, the objective is to test the input loop. It's not about creating the perfect calculator.

💡
This article was written and tested on a Mac.

Step 1. Create the project

Open up a terminal window and run these commands:

mkdir -p ~/projects/python/python-input-test
cd ~/projects/python/python-input-test

Step 2. Setup the dev container

For this project, I'm using a Microsoft Dev Container.  See this article if you aren't set up for that:

How to use Microsoft Dev Containers (Visual Studio Code, Python)
In this article, I will show you how to use Microsoft Dev Containers to build a simple “Hello World” application in Python on a Mac.
  • Make sure Docker is running:
open -a Docker
  • Open Visual Studio Code (VS Code) with this command (mind the dot (.) at the end of the command):
code .
  • Open the command palette (Cmd+Shift+P on a Mac) and search for: "Dev Containers: Add Development Container Configuration Files"
  • Choose the "Python 3" configuration and use the defaults – this will generate the necessary configuration files for Python development within a Dev Container
  • Click the button Reopen in Container when it appears
  • Inside VS Code, select View / Terminal
  • You should see a prompt like this:
vscode ➜ /workspaces/python-input-test $

Step 3. Create the calculator module

  • In VS Code, create a new file called calc.py in the root of your project directory
  • Edit calc.py, add the following code and save the file:
# Author: Mitch Allen

# -----------------------
# calc class

class Calc:
    
    # Constructor
    def __init__(self):
        self.acc = 0

    def showTotal(self):
        print(f"total: {self.acc:.2f}")

    def add(self,value):
        self.acc += value
        self.showTotal()

    def sub(self,value):
        self.acc -= value
        self.showTotal()

    def run(self):

        while True:

            line = input("Enter a command (add, sub, quit):\n:> ")
            
            args = line.split()

            if len(args) < 1:
                return
            
            command = args[0]

            value = 0.0

            if len(args) > 1:
                strValue = args[1]
                try: 
                    value = float(strValue)
                except ValueError:
                    print(f"{strValue} must be a floating point value!")
                    return
                
            match command:
                case 'add':
                    self.add(value)
                case 'sub':
                    self.sub(value)
                case 'quit':
                    print("Bye!")
                    return
                case _:
                    print("invalid command")
                
            print()  # new line to keep things clean

This defines a Python class called Calc.  It has the following methods:

  • the constructor - initialize the calculator by setting the accumulator (acc) to zero
  • showTotal - a method to show the current value of the accumulator by echoing it to the console
  • add - a method that takes a value as an argument and adds it to the accumulator, then calls showTotal to echo the new value to the console
  • sub - a method that takes a value as an argument and subtracts it from the accumulator, then calls showTotal to echo the new value to the console
  • run - the method that starts the input loop and waits for the user to enter a command

When the run method is called, the user can do things like this at the prompt:

:> add 5.1
:> sub 2.3
:> sub 1.1
:> quit
💡
Feel free to add more options to the Calc class as an exercise.

Step 4. Create the calc test file

  • Create a new file called test_calc.py in the root of your project directory
  • Edit test_calc.py, add the following code and save the file:
# Author: Mitch Allen

# To run:
#   python test_calc.py
#   python -m unittest test_calc.py
#   python -m unittest 

import io
import unittest
from unittest.mock import patch
from calc import Calc

class CalcTest(unittest.TestCase):

    def verify_output(self,user_input,expected_output):
        with patch('sys.stdout', new=io.StringIO()) as fake_stdout:
            with patch('builtins.input', side_effect=user_input):
                c = Calc()
                c.run()

            self.assertIn(expected_output, fake_stdout.getvalue())

    def test_quit(self):
        user_input = ['quit']
        expected_output = f"Bye!"
        self.verify_output(user_input,expected_output)

    def test_add(self):
        user_input = ['add 5', 'quit']
        expected_output = f"total: 5.0"
        self.verify_output(user_input,expected_output)

    def test_sub(self):
        user_input = ['sub 5', 'quit']
        expected_output = f"total: -5.00"
        self.verify_output(user_input,expected_output)

if __name__ == '__main__':
    unittest.main()

The code does the following:

  • Defines a test class called CalcTest
  • Defines a method called verify_output that generates input from an array of commands (user_input), then compares the console output against an expected string (expected_output)
  • Defines test cases that call verify_output with input command (user_input) and expected output (expected_output)

To test the file, run this command:

python test_calc.py

verify_output

The key to how the tests work is the verify_output method.

The method uses  unittest mock patch to mock input, based on an array of commands passed to the side_effect argument.

It also uses patch to capture the output of the run command to a variable called fake_stdout.

Finally, fake_stdout can be tested using self.assertIn to see if an expected string can be found in the captured output.

Step 5. Create the app file

To test the file manually do the following:

  • Create a new file called app.py in the root of your project directory
  • Edit app.py and add the following code and save the file:
# Author: Mitch Allen

from calc import Calc

# -----------------------
# main function

def app():
    c = Calc()
    c.run()

# -----------------------
# main entry point

if __name__ == "__main__": 
    app()

From the command line, run this command:

python app.py

Enter some commands to manually test it:

:> add 5.1
:> add 2.3
:> sub 1.1
:> sub 2.2
:> quit

Example Repo

The example used for this article can be found here:

I've also added to the example a unit test for testing the app directly as well.

💡
The example uses Microsoft Dev Containers. Be sure to run Docker before opening in VS Code.

Conclusion

In this article, you learned how to:

  • Create a class in Python to accept user input
  • Use unit test mocks to simulate user input and compare it against the expected output

References

  • docs.python.org/3/library/unittest.mock.html - [1]