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.
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
22import 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,
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:
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
14import typer
app = typer.Typer()
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,
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:
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.
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.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.
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()
andtyper.confirm()
, helping you develop more interactive CLI applications.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.
Argument Type Conversion: Argparse supports automatic type conversion based on the
type
parameter provided toadd_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
11rest:
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
66import 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 theArgumentParser
class from theargparse
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 theparse_args()
function and then theyaml_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
36import yaml
import typer
import os
app = typer.Typer()
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
11from 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:
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
12from 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:
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
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
75from 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.",
),
]
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",
),
]
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
52from 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()
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:
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
3def 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
31import 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
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:
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