How To Test CLI Applications With Pytest, Argparse And Typer

Have you ever struggled with testing command-line arguments for your applications or scripts?

Perhaps you’ve build a robust application, with a database and REST API and interfaced via command-line (CLI).

You’ve tested the database and REST API, but what about the CLI?

How do you test that your code correctly handles missing arguments, wrong data types, or invalid strings or characters?

Command Line Arguments are a prime error candidate for errors, given their immense interaction with the end user. Hence, its crucial to ensure your application correctly processes user inputs and handles errors gracefully.

How do you do this without redefining all arguments in your tests? How do you abstract that layer?

The good news is Pytest has you covered.

Whether you’re using Python libraries like Argparse or Typer, Pytest provides a variety of methods to test command-line arguments.

Options include passing a list of command-line values, power of Pytest parametrization, and Pytest addoption.

This article will take you through different ways to test your command-line arguments using a real example to help you understand and apply the core concept.

We’ll discuss not just the HOW but also WHAT to test and share some best practices when testing command-line based applications.

So, let’s begin.

Example Code

What You’ll Learn

This article will teach you to:

  • Use Python command-line libraries like Typer and Argparse
  • What to test in a CLI application.
  • How to test CLI applications with Pytest.
  • Automatically pass command-line arguments using Pytest Parametrization.
  • Manually pass command-line arguments using Pytest Addoption.
  • Abstract your CLI layer for testing.
  • Essential best practices in testing CLI applications.

Understanding Argparse

Before delving into the core topic, let’s do a brief overview of the popular Python library - Argparse.

Argparse is a standard Python library dedicated to parsing command-line arguments. With Argparse, crafting robust and intuitive command-line interfaces is simplified.

This utility enables the parsing of command-line arguments, options and documentation effortlessly.

Argparse helps you include several command-line parameters within a single option including subcommands.

However, it’s important to note that Argparse requires explicit type conversion and validation for command-line arguments.

While it does support the specification of argument types (e.g., type=int), users are required to manually manage type conversions and validation procedures.

Let’s quickly look at a basic argparse CLI example,

src/script_argparse.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import argparse

def main():
# Create an ArgumentParser object
parser = argparse.ArgumentParser(description="A simple greeting application")

# Add arguments
parser.add_argument("--name", help="The name of the person")
parser.add_argument("--age", type=int, help="The age of the person")

# Parse the arguments from the command line
args = parser.parse_args()


# Access the argument and print the greeting
if args.age:
print(f"Hello, {args.name}! You are {args.age} years old.")
else:
print(f"Hello, {args.name}!")

if __name__ == "__main__":
main()

Here we define a main function where we initiate an ArgumentParser object named parser. Using the add_argument() method, we define two arguments: --name and --age.

Upon invocation of the parse_args() method, the arguments --name and --age are parsed, and their respective values can be accessed using args.name and args.age.

Now, if you run the code following the below command:

1
python src/script_argparse.py --name=John --age=30

You’ll have something like this,

pytest-CLI-argument-test-example

If you need a reminder of the arguments, you can run the command with the --help flag,

1
python src/script_argparse.py --help

You’ll have the following output:

pytest-CLI-argument-test-example

You’ll know pretty much most libraries with a CLI layer use --help to generate a quick guide.

However, there are few issues with Argparse.

Argparse lacks important features like automated type conversion, interactive prompts, colorful output, and an automatic help generator.

That’s where a new library Typer comes in, containing all the features and functionalities required to create a highly interactive and user-friendly CLI application.

Typer was developed by the popular Sebastián Ramírez, who’s also the creator of FastAPI.

Typer: An Argparse Alternative

typer is an advanced Python library tailored for crafting CLI applications. Renowned for its simplicity, developer and user-friendliness compared to Argparse, it emerges as the optimal modern choice for building robust CLI solutions.

The primary objective is to speed up CLI development, capitalizing on Python’s type hints to enhance clarity and ease of use.

If we develop a similar greeting program that we just created using Argparse, it will look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import typer

app = typer.Typer()

@app.command()
def greet(name: str, age: int = 0):
"""Greet someone."""
if age:
typer.echo(f"Hello, {name}! You are {age} years old.")
else:
typer.echo(f"Hello, {name}!")

if __name__ == "__main__":
app()

As yo can see we’ve converted the method greet() to a CLI command through the docorator @app.command().

Now, if you run the code following the below command:

1
python src/script_typer.py John --age=30 

You’ll have something like this,

pytest-CLI-argument-test-example

If the user needs help with arguments, they can easily generate a quick guide using the following command,

1
python src/script_typer.py --help

You’ll have the following output:

pytest-CLI-argument-test-example

Here is a brief note on arguments in general:

Required Arguments:
These are arguments for which no default value is specified.

They must be provided by the user when invoking the CLI command.

In the example provided, name is a required argument.

This is because the function greet is defined with name: str without a default value, indicating that the CLI command must be called with a name value for it to execute properly.

Optional Arguments:
Optional arguments have a default value defined in the function signature.

These arguments do not need to be explicitly provided by the user, as the default value will be used if the argument is omitted.

In the code example, age is an optional argument because it is defined with a default value (age: int = 0).

This means the user can omit the age argument when calling the command, and 0 will be used as the default age.

Positional Arguments:

In the context of Typer and CLI applications, arguments are considered positional based on the order they appear in the function definition.

Typer expects these arguments in the order they are defined.

Both name and age in the example are positional arguments from the CLI’s perspective.

Overall

In the provided example, name is a required argument because it lacks a default value.

age is an optional argument because it has a default value (0).

The user must provide name when invoking the greet command, but can choose to provide age either by its position or by using the --age flag.

Argparse vs Typer

Argparse and Typer are both libraries designed for crafting command-line interfaces (CLIs), each with its own unique characteristics. Let’s see which one is ideal for creating a CLI application.

  1. Defining CLI Arguments: In Argparse, defining CLI arguments involves calling specific functions, whereas Typer simplifies this process by enabling the conversion of functions into CLI arguments using the @typer.command() decorator.

  2. Ease of Use: Argparse has a more verbose syntax. In contrast, Typer has a concise and intuitive syntax, helping you create complex CLI applications with ease.

  3. Interactivity: While Argparse primarily focuses on parsing command-line arguments and generating help messages, Typer stands out by supporting interactive prompts through functionalities such as typer.prompt() and typer.confirm(), helping you develop more interactive CLI applications.

  4. Colorful Output: Typer, built on top of Click, supports colorful output without additional dependencies, while Argparse would require external libraries (e.g., colorama or termcolor) to achieve colored text in the terminal.

  5. Argument Type Conversion: Argparse supports automatic type conversion based on the type parameter provided to add_argument(). For example, parser.add_argument('--age', type=int) automatically converts the input to an integer. The key difference with Typer is its reliance on Python type hints for automatic type inference, which streamlines the declaration of argument types without explicitly calling out the type conversion as in Argparse.

Practical Example

Let’s get started with a practical example.

Prerequisites

Some basics of Python and Pytest would be helpful:

  • Python (3.11+)
  • Pytest

Getting Started

Our example repo looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.  
├── .gitignore
├── README.md
├── pytest.ini
├── requirements.txt
├── tests
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_argparse_parametrization.py
│ ├── test_typer_parametrization.py
│ ├── test_yaml_reader_custom_cli.py
│ └── test_yaml_reader_list.py
└── src
├── yaml_configs
│ └── config.yml
├── argparse_yaml_reader.py
├── script_argparse.py
├── script_typer.py
└── typer_yaml_reader.py

We have a src folder containing the main code and a tests folder containing the test code.

Our example code is a simple YAML reader that accepts a path to a YAML file as a command-line argument and reads the file.

To get started. clone the Github Repo here, or you can create your own repo by creating a folder and running git init to initialize it.

Create a virtual environment and install the required packages using the following command:

1
pip install -r requirements.txt

Feel free to use any package manager you wish.

Example Code

Our example code is a basic YAML reader developed using the Argparse and Typer libraries.

There are 2 functions - one to read the command-line argument (path to config file) and another to read the config file itself.

Here’s a simple YAML config file containing some rest API configurations for different environments,

src/yaml_configs/config.yml

1
2
3
4
5
6
7
8
9
10
11
rest:
url: "https://example.com/"
port: 3001

dev:
url: "https://dev.com/"
port: 3010

prod:
url: "https://prod.com/"
port: 2007

Argparse Example

First, we’ll look at the YAML reader with the Argparse library.

src/argparse_yaml_reader.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import os
import yaml
from typing import Dict
import argparse
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter


def parse_args(args=None) -> ArgumentParser.parse_args:
"""
Function to parse command line arguments

Args:
args: list of strings to parse

Returns:
parsed_args: parsed arguments
"""
argument_parser = ArgumentParser(
description="Command line arguments for reading a configuration file",
formatter_class=ArgumentDefaultsHelpFormatter,
)
argument_parser.add_argument(
"--configpath", type=str, help="Configuration file path", required=True)
argument_parser.add_argument(
"--env", type=str,default="rest", help="Select environment")
return argument_parser.parse_args(args)


def yaml_reader(path: str, env:str) -> Dict:
"""
Function to read YAML config file

Args:
path: path to the YAML file

Returns:
data: dictionary of data from the YAML file
"""
try:
with open(path, "r") as yamlfile:
data = yaml.load(yamlfile, Loader=yaml.FullLoader)
return data[env]
except Exception as e:
print(f"Error reading YAML file: {e}")


def main(args=None) -> None:
"""
Main function to read YAML file
"""
args = parse_args(args)
configpath = args.configpath
env = args.env

if len(configpath) == 0:
print("No path provided")
else:
if configpath and os.path.isfile(configpath):
print(yaml_reader(path=configpath, env=env))
else:
print(
f"`configpath` must be a valid file path. Provided path: `{configpath}` does not exist."
)

if __name__ == "__main__":
main()

We have 3 functions here:

  • parse_args(): This function parses the command-line arguments using the ArgumentParser class from the argparse library. It returns the parsed arguments.
  • yaml_reader(): This function reads the YAML file and returns the data based on the environment provided.
  • main(): This is the main function that calls the parse_args() function and then the yaml_reader() function.

Typer Example

Let’s write the same operation using Typer.

src/typer_yaml_reader.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import yaml
import typer
import os
app = typer.Typer()

@app.command()
def main(configpath: str, env:str = 'rest') -> None:
"""
Main function to read YAML file

Args:
configpath: path to the YAML file

Returns:
None
"""
if configpath and os.path.isfile(configpath):
print(yaml_reader(configpath, env))
else:
print(
f"`configpath` must be a valid file path. Provided path: `{configpath}` does not exist."
)

def yaml_reader(path: str, env:str) -> None:
"""
Function to read YAML config file
"""
try:
with open(path, "r") as yamlfile:
data = yaml.load(yamlfile, Loader=yaml.FullLoader)
return data[env]
except Exception as e:
print(f"Error reading YAML file: {e}")

if __name__ == "__main__":
app()

You can straightaway see how much cleaner Typer is.

Here, app() serves as the main function within the typer object.

What To Test - Strategy

Testing isn’t just about perfect functionality, it’s also about ensuring correct user interaction. Let’s quickly explore some strategies for effective application testing:

  • Test Incorrect Argument Values: Test that your application can handle incorrect argument values e.g. missing file locations

    1
    test_incorrect_args = ["--configpath", "./path/to/nonexistent/file.yml"]
  • Test Missing Arguments: Test that your application can handle missing arguments.

    1
    test_missing_args = [" "]
  • Test Datatypes: Check if your application validates the argument datatypes correctly.

    1
    test_incorrect_args = ["--configpath", True]

    Here, the argument --configpath requires a string but can raise an error if a boolean is passed.

  • Test Null Values: Test if your application can handle null values or empty strings.

    1
    test_incorrect_args = ["--configpath", ""]
  • Test Typing Mistakes: Test if your application can handle typing mistakes and return the correct error message.

    1
    test_args = ["--typo", "src/yaml_configs/config.yml"]
  • Test Exceptions: Test if your application can handle exceptions like FileNotFoundError or Permission Errors and return the correct error message.

These are just some of the test case scenarios to think about but I encourage you to think about all possible cases even before you write your tests.

Even better, make use of property-based testing tools like Hypothesis to generate test cases for you.

3 Ways to Test Command-Line Arguments

Now that we have a strategy in mind, let’s explore 3 different ways to test command-line arguments using Pytest.

The first being a simple list-based testing, followed by automated parameterized testing, and finally manual testing with Pytest Addoption.

List-Based Testing

This is the most basic way to test command-line arguments.

You can pass arguments as a list to the main function.

tests/test_yaml_reader_list.py

1
2
3
4
5
6
7
8
9
10
11
from src.argparse_yaml_reader import main
from typer.testing import CliRunner
from src.typer_yaml_reader import app


def test_argparse_yaml_with_list(capsys):
test_args = ['--configpath', 'src/yaml_configs/config.yml', '--env', 'rest']
expected_output = "{'url': 'https://example.com/', 'port': 3001}"
main(test_args)
output = capsys.readouterr().out.rstrip()
assert expected_output in output

The capsys fixture captures stdout and stderr output during the execution of test functions allowing you to access it and perform assertions against expected outputs.

If you’re unfamiliar with capsys and it’s different modes, we have you covered here.

Now run the test:

1
pytest -v tests/test_yaml_reader_list.py

You’ll have the following result:

pytest-CLI-argument-test-example-result

Now, let’s test the Typer YAML reader using the same method.

tests/test_yaml_reader_list.py

1
2
3
4
5
6
7
8
9
10
11
12
from typer.testing import CliRunner
from src.typer_yaml_reader import app

def test_typer_yaml_with_list():
runner = CliRunner()
test_args = ['src/yaml_configs/config.yml', '--env', 'rest']
result = runner.invoke(app, test_args)
assert result.exit_code == 0
# Use result.stdout to access the command's output
output = result.stdout.rstrip()
expected_output = "{'url': 'https://example.com/', 'port': 3001}"
assert expected_output in output

Typer provides a CliRunner() object to invoke the command and capture the output.

When running the test:

1
pytest -v tests/test_yaml_reader_list.py

You’ll have the following result:

pytest-CLI-argument-test-example-result

Parametrized Testing

Pytest parametrization allows you to run tests against a variety of input data, without the need to write multiple tests.

Using the decorator @pytest.mark.parameterize, you can specify the inputs and expected outputs for tests.

Simple example,

1
2
3
4
5
6
# content of test_expectation.py
import pytest

@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, expected):
assert eval(test_input) == expected

You can use any type of value, such as numbers, strings, lists, or dictionaries.

For a deep exploration of Pytest Parameterization, check out this comprehensive guide.

Now, for our example code, we can parameterize tests as follows,

Argparse Example

  • Please see the comments next to each parameterized test for a brief explanation of the test case.

tests/test_argparse_parametrization.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
from src.argparse_yaml_reader import main
import pytest
import shlex

test_cases = [
(
"--configpath='src/yaml_configs/config.yml'", # Valid path without optional args
"{'url': 'https://example.com/', 'port': 3001}",
),
(
"--configpath 'src/yaml_configs/config.yml' --env='dev'", # Valid path with optional args
"{'url': 'https://dev.com/', 'port': 3010}",
),
(
"--env='prod' --configpath 'src/yaml_configs/config.yml'", # Different order
"{'url': 'https://prod.com/', 'port': 2007}",
),
(
"--configpath 'src/config.yml' --env='dev'", # Path doesn't exist
"`configpath` must be a valid file path. Provided path: `src/config.yml` does not exist.",
),
(
"--configpath ''", # Null or None value passed
"No path provided",
),
(
"--configpath 'src/yaml_configs==config.yml'", # Invalid path
"`configpath` must be a valid file path. Provided path: `src/yaml_configs==config.yml` does not exist.",
),
(
"--configpath 'path/to/nonexistent/file.yml'", # Nonexistent file
"`configpath` must be a valid file path. Provided path: `path/to/nonexistent/file.yml` does not exist.",
),
]


@pytest.mark.parametrize("command, expected_output", test_cases)
def test_argparse_yaml_reader(capsys, command, expected_output):
main(shlex.split(command))
captured = capsys.readouterr()
output = captured.out + captured.err
assert expected_output in output

# Test cases
test_cases_sys_exit = [
(
"", # No argument passed
"the following arguments are required: --configpath",
),
(
"-configpath 'src/yaml_configs/config.yml' --env 'dev'", # Wrong flag passed
"the following arguments are required: --configpath",
),
(
"configpath 'src/yaml_configs/config.yml' --env 'dev'", # No flag passed
"the following arguments are required: --configpath",
),
(
"-+configpath 'src/yaml_configs/config.yml' --env 'dev'", # Wrong Type of flag passed
"the following arguments are required: --configpath",
),
(
"--wrong_argument 'src/yaml_configs/config.yml' --env 'dev'", # Wrong argument name
"the following arguments are required: --configpath",
),
]


@pytest.mark.parametrize("command, expected_output", test_cases_sys_exit)
def test_argparse_yaml_reader_sys_exit(capsys, command, expected_output):
with pytest.raises(SystemExit): # Expecting SystemExit due to argparse error
main(shlex.split(command))
captured = capsys.readouterr() # Capture both stdout and stderr
output = captured.out + captured.err # Combine stdout and stderr
assert expected_output in output

Here, the list test_cases and test_cases_sys_exit contains a set of test cases for the Argparse YAML reader.

Note that we used with pytest.raises(SystemExit) to capture the SystemExit exception raised by the argparse library when an error occurs.

We can perform the same thing using the Typer YAML reader as follows:

Typer Example

tests/test_typer_parametrization.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from typer.testing import CliRunner
from src.typer_yaml_reader import app
import pytest
import shlex

runner = CliRunner()

# Test cases with location and expected result
test_cases = [
(
"src/yaml_configs/config.yml", # Valid path without optional args
"{'url': 'https://example.com/', 'port': 3001}",
),
(
"src/yaml_configs/config.yml --env 'dev'", # Valid path witho optional args
"{'url': 'https://dev.com/', 'port': 3010}",
),
(
"--env 'prod' 'src/yaml_configs/config.yml'", # Different order
"{'url': 'https://prod.com/', 'port': 2007}",
),
(
"src/config.yml --env 'prod'", # Path not exist
"`configpath` must be a valid file path. Provided path: `src/config.yml` does not exist.",
),
(
" ", # Null or None value passed
"Missing argument",
),
(
"", # No argument passed
"Missing argument",
),
(
"'src/yaml_configs/config.yml' -env 'dev'", # Invalid flag
"No such option",
),
(
"src/yaml_configs==config.yml --env 'dev'", # Invalid ascii character passsed
"`configpath` must be a valid file path. Provided path: `src/yaml_configs==config.yml` does not exist.",
),
(
"path/to/nonexistent/file.yml --env 'dev'", # Nonexistent file
"`configpath` must be a valid file path. Provided path: `path/to/nonexistent/file.yml` does not exist.",
),
]

# Testing typer_yaml_reader()
@pytest.mark.parametrize("command, expected_output", test_cases)
def test_typer_yaml_reader(command, expected_output):
result = runner.invoke(app, shlex.split(command))
assert expected_output in result.stdout

Same as before, the variable test_cases stores a list of tests with expected output.

Typer also provides a testing object CliRunner to invoke the command and capture the output.

The library method shlex.split() breaks the test case text based on spaces.

Running both tests:

1
pytest -v tests/test_argparse_parametrization.py tests/test_typer_parametrization.py

You’ll have the following result:

pytest-CLI-argument-test-example-result

Manual Testing with Pytest Addoption

Pytest Addoption allows you to define custom CLI arguments for tests. These arguments can be used to modify the behavior of your tests or pass configuration parameters to your test function or fixtures.

Note that this method does NOT abstract the command-line layer from your tests like the previous methods.

Let’s go ahead and implement this strategy.

tests/conftest.py

1
2
3
def pytest_addoption(parser):
parser.addoption("--configpath", action="store", help="Location to YAML file")
parser.addoption("--env", action="store", help="Environment to read from YAML file")

The conftest.py file contains the test arguments which are then processed by a fixture as follows. If you need a refresher on conftest.py check out this guide.

tests/test_yaml_reader_custom_cli.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import pytest
import shlex
from src.argparse_yaml_reader import main, yaml_reader
from typer.testing import CliRunner
from src.typer_yaml_reader import app


# Fixture to get user command-line arguments
@pytest.fixture
def get_user_input(request):
configpath = str(request.config.getoption("--configpath"))
env = str(request.config.getoption("--env"))
return configpath, env


# Testing argparse_yaml_reader()
def test_argparse_yaml_reader(capsys, get_user_input):
configpath, env = get_user_input
expected_output = str(yaml_reader(configpath, env))
main(shlex.split("--configpath " + configpath + " --env " + env))
output = capsys.readouterr().out.rstrip()
assert expected_output in output


# Testing typer_yaml_reader()
def test_typer_yaml_reader(get_user_input):
configpath, env = get_user_input
expected_output = str(yaml_reader(configpath, env))
runner = CliRunner()
result = runner.invoke(app, shlex.split(configpath + " --env " + env))
assert expected_output in result.stdout

Here, the get_user_input() fixture processes command-line arguments and passes them to the tests.

When running the test you can provide arguments with the necessary value:

1
pytest -v tests/test_yaml_reader_custom_cli.py --configpath="src/yaml_configs/config.yml" --env="dev"

You can also include these options with adopts in the pytest.ini file.

This is an excellent approach (and my preferred one) when you have a lot of tests to run manually:

1
2
3
4
[pytest]
addopts =
--configpath="src/yaml_configs/config.yml"
--env="prod"

Then run it using:

1
pytest -v tests/test_yaml_reader_custom_cli.py

You’ll have the following result:

pytest-CLI-argument-test-example-result

Best Practices for Testing CLI Arguments

Let’s unwrap some tips and best practices for testing CLI applications.

Parametrized Tests:
Employ parameterized tests to cover various input scenarios with different command-line argument combinations. This approach aids in uncovering unexpected behavior or edge cases that your CLI application may encounter. If you can, use property-based testing tools like Hypothesis.

Error Handling:
Thoroughly test error handling and exception scenarios to validate the handling of invalid command-line arguments. Ensure error messages, handlers, and exit codes align with expectations.

Use Configuration Files:
If you attempt to run a test manually, use configuration files like pytest.ini, tox.ini, setup.cfg, and pyproject.toml. If you’re unfamiliar with them, go through this comprehensive guide. This approach reduces the need to pass CLI commands manually on each run eliminating possibility for errors and also keeps config consistent.

User Focused Test Cases:
The test cases should be from the user’s perspective. Design test cases that demonstrate common mistakes and errors like typos, empty arguments, wrong data types, missing arguments and more. This practice ensures that the application behaves as expected when critical errors occur.

Prioritize Feature Testing:
Develop a strategic approach to prioritize feature tests based on their significance, such as new features, core functionalities, security measures, reporting capabilities, and other advanced features. This approach ensures efficient resource allocation and focuses on critical aspects.

Wrapping Up

That’s all about testing CLI applications.

It’s been a long but insightful and practical journey.

This article provides insights into using Pytest to test command-line applications built with popular Python libraries such as Argparse and Typer.

Illustrated through practical examples, you explored the aspects of testing CLI applications using diverse methods like lists, parametrization, and addoption.

You also learned about the best practices including error handling, user-focused test cases, config files and feature prioritization.

If you’re wondering what I use - depends on your use case but I like to use a combination of list based arguments with parameterization, as I’m a big fan of DRY (Don’t Repeat Yourself), automated testing and source code abstraction from the test layer.

For temporary manual testing with different arguments, Pytest addoption is a nice choice.

Go ahead and try the examples in your local environment. Happy Testing! 🚀🐍

If you have any ideas for improvement or like me to cover any topics please comment below or send me a message via Twitter, GitHub or Email.

Till the next time… Cheers!

Additional Readings

Example Code
Typer - Official Docs
Argparse - Official Docs
The Ultimate Guide To Capturing Stdout/Stderr Output In Pytest
How to Effortlessly Generate Unit Test Cases with Pytest Parameterized Tests
How To Use Pytest With Command Line Options (Easy To Follow Guide)
How to Use Hypothesis and Pytest for Robust Property-Based Testing in Python
What Is pytest.ini And How To Save Time Using Pytest Config
Pytest Config Files - A Practical Guide To Good Config Management
Pytest Conftest With Best Practices And Real Examples
Building and Testing FastAPI CRUD APIs with Pytest - A Step-By-Step Guide
How To Test Database Transactions With Pytest And SQLModel
Testing argparse Applications
Build Command-Line Interfaces With Python’s argparse
Python: Better CLIs with Typer