python tutorial python-click pytest

It's good practice to, as much as possible, write tests for your code. If you're working with Python, pytest makes the process of writing and running tests much smoother. I wrote a few posts some time back on getting started with testing with pytest, so if you're completely new to it, you might want to take a look at them:

For testing CLI apps, Click provides a convenient module: click.testing which has some useful functions (notably CliRunner()) to help us invoke commands and check their behavior.

We'll go ahead and test each part of our app - creating, reading, updating and deleting.

Installing pytest and writing the first test

pytest can be installed via pip:

  (env) $ pip install pytest

After installing pytest, create a tests folder in the root directory and add the first test file:

  (env) $ mkdir tests && cd tests

  (env) $ touch

In the test_app file, add the following code for a start:

  def test_add():

To run the test, run pytest on the terminal:

  (env) $ pytest
  ================== test session starts ====================
  platform linux -- Python 3.7.3, pytest-5.1.0, py-1.8.0, pluggy-0.12.0
  rootdir: /home/wangonya/code/contacts-cli
  collected 1 item

  tests/ .                              [100%]

  ================== 1 passed in 0.04s =======================

Testing the add command

Let's edit the test_app file to add a test to see if the add command adds a new contact:

  from click.testing import CliRunner

  from app import add

  runner = CliRunner()

  def test_add():
      response = runner.invoke(add, ["test-user", "-m", "0"])
      assert response.exit_code == 0
      assert "Contact test-user added!" in response.output
      assert "{'mobile': '0'}" in response.output

First, we invoke the command as we would on the terminal, passing in the required arguments and options: response = runner.invoke(add, ["test-user", "-m", "0"]).

We then check that the command executes successfully: assert response.exit_code = 0=.

If the command executes successfully, we expect a success message should be returned in the response with the values we added:

  assert "Contact test-user added!" in response.output
  assert "{'mobile': '0'}" in response.output

The rest of the tests will pretty much follow the same format.

Testing the list command

  def test_list():
      response = runner.invoke(list)
      assert response.exit_code == 0
      assert "Here\'s a list of all your contacts:" in response.output
      assert "'test-user': {'mobile': '0'}" in response.output

The list command doesn't take any arguments or options so we just call it directly: response = runner.invoke(list).

Testing the view command

  def test_view():
      response = runner.invoke(view, "test-user")
      assert response.exit_code == 0
      assert "{'mobile': '0'}" in response.output

Testing the update command

  def test_update():
      response = runner.invoke(update, ["test-user", "-m", "12345"])
      assert response.exit_code == 0
      assert "Contact updated!" in response.output
      assert "{'mobile': '12345'}" in response.output

Testing the delete command

  def test_delete():
      response = runner.invoke(delete, "test-user")
      assert response.exit_code == 0
      assert "Contact deleted!" in response.output

      # call view on test-user to confirm it doesn't exist
      response = runner.invoke(view, "test-user")
      assert response.exit_code == 0
      assert "The contact you searched for doesn't exist" in response.output


As your application grows, you may want to consider using fixtures and set up things like runner in a file. We got away with it here because our tests were simple and all in a single file. Once multiple test files are introduced, following the approach we used here would lead to a lot of unnecessarily duplicated code.

Also, we made direct calls to our API in the tests. This operation should ideally be mocked.