diff --git a/bob/io/base/test/test_utlilities.py b/bob/io/base/test/test_utlilities.py
new file mode 100644
index 0000000000000000000000000000000000000000..d5f3da12e3441cb430eb97a213c17055c825aa97
--- /dev/null
+++ b/bob/io/base/test/test_utlilities.py
@@ -0,0 +1,38 @@
+import sys
+
+import click
+
+from click.testing import CliRunner
+
+from bob.io.base import test_utils
+
+
+@click.command("dummy")
+def dummy_command_0():
+    sys.exit(0)
+
+
+@click.command("dummy_exit_1")
+def dummy_command_1():
+    sys.exit(1)
+
+
+@click.command("dummy_exit_raise")
+def dummy_command_raise():
+    raise RuntimeError("Expected exception")
+
+
+def test_assert_dummy():
+    result = CliRunner().invoke(dummy_command_0)
+    assert result.exit_code == 0
+    test_utils.assert_click_runner_result(result)
+
+    result = CliRunner().invoke(dummy_command_1)
+    assert result.exit_code == 1
+    test_utils.assert_click_runner_result(result, exit_code=1)
+
+    result = CliRunner().invoke(dummy_command_raise)
+    assert result.exit_code == 1
+    test_utils.assert_click_runner_result(
+        result, exit_code=1, exception_type=RuntimeError
+    )
diff --git a/bob/io/base/test_utils.py b/bob/io/base/test_utils.py
index 6156c4b459b6e10085b557471447daadadfb346b..193b3239c1d43b23a29fd189c55b201b1aa9ae2c 100644
--- a/bob/io/base/test_utils.py
+++ b/bob/io/base/test_utils.py
@@ -10,8 +10,13 @@
 
 import functools
 import os
+import traceback
 import unittest
 
+from typing import Optional
+
+import click.testing
+
 
 def datafile(f, module=None, path="data"):
     """datafile(f, [module], [data]) -> filename
@@ -117,3 +122,38 @@ def extension_available(extension):
         return wrapper
 
     return test_wrapper
+
+
+def assert_click_runner_result(
+    result: click.testing.Result,
+    exit_code: int = 0,
+    exception_type: Optional[Exception] = None,
+):
+    """Helper for asserting click runner results.
+
+    Parameters
+    ----------
+    result
+        The return value on ``click.testing.CLIRunner.invoke()``.
+    exit_code
+        The expected command exit code (defaults to 0).
+    exception_type
+        If given, will ensure that the raised exception is of that type.
+    """
+
+    m = (
+        "Click command exited with code '{}', instead of '{}'.\n"
+        "Exception:\n{}\n"
+        "Output:\n{}"
+    )
+    exception = (
+        "None"
+        if result.exc_info is None
+        else "".join(traceback.format_exception(*result.exc_info))
+    )
+    m = m.format(result.exit_code, exit_code, exception, result.output)
+    assert result.exit_code == exit_code, m
+    if exit_code == 0:
+        assert not result.exception, m
+    if exception_type is not None:
+        assert isinstance(result.exception, exception_type), m
diff --git a/conda/meta.yaml b/conda/meta.yaml
index 7f9bf577668dc920a97055fe5f0d5ac715b38f0c..a70a3cf70270a8c7579dbeb4556806719430b663 100644
--- a/conda/meta.yaml
+++ b/conda/meta.yaml
@@ -26,6 +26,7 @@ requirements:
     - numpy {{ numpy }}
     - pillow {{ pillow }}
     - imageio {{ imageio }}
+    - click {{ click }}
   run:
     - python
     - setuptools
@@ -33,6 +34,7 @@ requirements:
     - {{ pin_compatible('pillow') }}
     - {{ pin_compatible('imageio') }}
     - {{ pin_compatible('h5py') }}
+    - {{ pin_compatible('click') }}
 
 test:
   imports:
diff --git a/requirements.txt b/requirements.txt
index 32506f993451e5f98c4342c48604b630ef0e59d3..ae1697a953f3f2dd3a8083aef515046b247fcffc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,3 +2,4 @@ bob.extension
 h5py
 imageio
 numpy
+click