If you enjoyed this book considering buying a copy
- Buy a copy of the book on Lean Pub
- Buy a copy of the book on Kindle
- Buy a hard copy of the book on Amazon
- Buy the bundle Master Python on Lean Pub
Chapter 2: Test with Click #
Noah Gift
Test a small click app #
Writing command-line tools is one of my favorite ways to write code. A command-line tool is one of the simplest ways to take programming logic and create a flexible tool. Let’s walk through a simple click
tool.
The following is an intentionally simple command-line tool that takes a flag --path
(path to a file) and flag --ftype
(file type). If this runs without specifying, it will prompt a user for both of these flags.
{caption: “Simple click
based cli that searches for files: gcli.py
"}
#!/usr/bin/env python
import click
import glob
# this is bad code intentionally
# varbad=
@click.command()
@click.option(
"--path",
prompt="Path to search for files",
help="This is the path to search for files: /tmp",
)
@click.option(
"--ftype", prompt="Pass in the type of file", help="Pass in the file type: i.e csv"
)
def search(path, ftype):
results = glob.glob(f"{path}/*.{ftype}")
click.echo(click.style("Found Matches:", fg="red"))
for result in results:
click.echo(click.style(f"{result}", bg="blue", fg="white"))
if __name__ == "__main__":
# pylint: disable=no-value-for-parameter
search()
Another useful feature of click
is the --help
flag comes for free and autogenerates from the options. This following is the output from ./glci --help
.
{caption: “Output of ./gcli --help
"}
(.tip) $ click-testing git:(master) $ ./gcli.py --help
Usage: gcli.py [OPTIONS]
Options:
--path TEXT This is the path to search for files: /tmp
--ftype TEXT Pass in the file type: i.e csv
--help Show this message and exit.
Next up is to run ./gcli.py
and let it prompt us for both the path
and the file type.
{caption: “Output of search with prompts”}
(.tip) $ click-testing git:(master) $ ./gcli.py
Path to search for files: .
Pass in the type of file: py
Found Matches:
./gcli.py
./test_gcli.py
./hello-click.py
./hello-click2.py
Another run of ./gcli.py
shows how it can be run by passing in the flags ahead of time.
{caption: “Output of search with flags”}
(.tip) $ click-testing git:(master) $ ./gcli.py --path . --ftype py
Found Matches:
./gcli.py
./test_gcli.py
./hello-click.py
./hello-click2.py
So can we test this command-line tool? Fortunately, the authors of click
have an easy solution for this as well. In a nutshell, by using the line from click.testing import CliRunner
it invokes a test runner as shown.
{caption: “Test file: test_gcli.py
"}
from click.testing import CliRunner
from gcli import search
# search(path, ftype):
def test_search():
runner = CliRunner()
result = runner.invoke(search, ["--path", ".", "--ftype", "py"])
assert result.exit_code == 0
assert ".py" in result.output
Like pytest,
a simple assert
statement is the only thing needed to create a test. Two different types of assert
comments show. The first assert
checks the result of the call to the command line tool and ensures that it returns a shell exit status of 0
. The second assert parses the output returned and ensures that .py
is in the production since the file type passed in is py.
When the command make test
is run, it generates the following output. Again take note of how using the convention make test
simplifies the complexity of remembering what exact flags to run. This step can set once and then forget about it.
{caption: “Output of make test
"}
(.tip) $ click-testing git:(master) make test
python -m pytest -vv --cov=gcli test_gcli.py
========================================= test session starts =================
platform darwin -- Python 3.7.6, pytest-5.3.2, py-1.8.1, pluggy-0.13.1 --
cachedir: .pytest_cache
rootdir: /Users/noahgift/testing-in-python/chapter11/click-testing
plugins: cov-2.8.1
collected 1 item
test_gcli.py::test_search PASSED
---------- coverage: platform darwin, Python 3.7.6-final-0 -----------
Name Stmts Miss Cover
-----------------------------
gcli.py 11 1 91%
========================================== 1 passed in 0.03s ==================