diff --git a/.gitignore b/.gitignore
index b8b187e..896a06a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ deb_dist/*
 .idea
 *.iml
 *.pyc
+.tox
diff --git a/.travis.yml b/.travis.yml
index 4374097..817f35a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,8 @@
 language: python
-python:
-- '3.4'
-script: python3 setup.py test
+install:
+  - pip3 install --user virtualenv
+script:
+  - python3 setup.py test
 deploy:
   provider: pypi
   user: typesafehub
diff --git a/conductr_cli/conduct_logging.py b/conductr_cli/conduct_logging.py
index 159999f..3d9feb8 100644
--- a/conductr_cli/conduct_logging.py
+++ b/conductr_cli/conduct_logging.py
@@ -26,7 +26,7 @@ def connection_error(err, args):
 
 def pretty_json(s):
     s_json = json.loads(s)
-    print(json.dumps(s_json, sort_keys=True, indent=2))
+    print(json.dumps(s_json, sort_keys=True, indent=2, separators=(',', ': ')))
 
 
 def handle_connection_error(func):
diff --git a/conductr_cli/shazar.py b/conductr_cli/shazar.py
index 5c97b3d..cac7342 100755
--- a/conductr_cli/shazar.py
+++ b/conductr_cli/shazar.py
@@ -11,10 +11,10 @@ import tempfile
 import zipfile
 
 
-def run():
+def run(argv=None):
     parser = build_parser()
     argcomplete.autocomplete(parser)
-    args = parser.parse_args()
+    args = parser.parse_args(argv)
     args.func(args)
 
 
@@ -45,10 +45,8 @@ def shazar(args):
         else:
             zip_file.write(args.source, source_base_name)
 
-    dest = shutil.move(
-        temp_file,
-        os.path.join(args.output_dir, '{}-{}.zip'.format(source_base_name, create_digest(temp_file)))
-    )
+    dest = os.path.join(args.output_dir, '{}-{}.zip'.format(source_base_name, create_digest(temp_file)))
+    shutil.move(temp_file, dest)
     print('Created digested ZIP archive at {}'.format(dest))
 
 
diff --git a/setup.py b/setup.py
index 535fada..03f23f9 100644
--- a/setup.py
+++ b/setup.py
@@ -1,10 +1,46 @@
 import os
+import sys
 from setuptools import setup, find_packages
 from conductr_cli import __version__
+from setuptools.command.test import test
 
 # allow setup.py to be run from any path
 os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
 
+install_requires = [
+    'requests>=2.3.0',
+    'argcomplete>=0.8.1',
+    'pyhocon==0.2.1'
+]
+if sys.version_info[:2] == (3, 2):
+    install_requires.extend([
+        'pathlib==1.0.1',
+        'mock==1.0.1'
+    ])
+
+
+class Tox(test):
+    user_options = [('tox-args=', 'a', 'Arguments to pass to tox')]
+
+    def initialize_options(self):
+        test.initialize_options(self)
+        self.tox_args = None
+
+    def finalize_options(self):
+        test.finalize_options(self)
+        self.test_args = []
+        self.test_suite = True
+
+    def run_tests(self):
+        # import here, cause outside the eggs aren't loaded
+        import tox
+        import shlex
+        args = self.tox_args
+        if args:
+            args = shlex.split(self.tox_args)
+        errno = tox.cmdline(args=args)
+        sys.exit(errno)
+
 setup(
     name='conductr-cli',
     version=__version__,
@@ -17,13 +53,12 @@ setup(
         ],
     },
 
-    install_requires=[
-        'requests>=2.3.0',
-        'argcomplete>=0.8.1',
-        'pyhocon==0.2.1'
-    ],
+    install_requires=install_requires,
+    tests_require=['tox'],
     test_suite='conductr_cli.test',
 
+    cmdclass={'test': Tox},
+
     license='Apache 2',
     description='A CLI client for Typesafe ConductR',
     url='https://github.com/typesafehub/conductr-cli',
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..5b12aef
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,6 @@
+[tox]
+envlist = py32, py34
+
+[testenv]
+deps = nose
+commands = nosetests

Improve CLI message
From @rpokrywka 

shazar: “Created digested ZIP archive at None” - the message should be giving more information.

@rpokrywka Can you provide a reproducible test case?
diff --git a/conductr_cli/test/cli_test_case.py b/conductr_cli/test/cli_test_case.py
index cbad63c..7467a38 100644
--- a/conductr_cli/test/cli_test_case.py
+++ b/conductr_cli/test/cli_test_case.py
@@ -1,9 +1,13 @@
 import os
 import shutil
 import tempfile
-from unittest.mock import MagicMock
 from requests.exceptions import ConnectionError, HTTPError
 
+try:
+    from unittest.mock import MagicMock  # 3.3 and beyond
+except ImportError:
+    from mock import MagicMock
+
 
 class CliTestCase():
     """Provides test case common functionality"""
diff --git a/conductr_cli/test/test_conduct_info.py b/conductr_cli/test/test_conduct_info.py
index 6188067..234f94d 100644
--- a/conductr_cli/test/test_conduct_info.py
+++ b/conductr_cli/test/test_conduct_info.py
@@ -1,8 +1,12 @@
 from unittest import TestCase
-from unittest.mock import patch, MagicMock
 from conductr_cli.test.cli_test_case import CliTestCase, strip_margin
 from conductr_cli import conduct_info
 
+try:
+    from unittest.mock import patch, MagicMock  # 3.3 and beyond
+except ImportError:
+    from mock import patch, MagicMock
+
 
 class TestConductInfoCommand(TestCase, CliTestCase):
 
diff --git a/conductr_cli/test/test_conduct_load.py b/conductr_cli/test/test_conduct_load.py
index 378215f..eb3b983 100644
--- a/conductr_cli/test/test_conduct_load.py
+++ b/conductr_cli/test/test_conduct_load.py
@@ -1,10 +1,14 @@
 from unittest import TestCase
-from unittest.mock import call, patch, MagicMock
 from conductr_cli.test.cli_test_case import CliTestCase, create_temp_bundle, create_temp_bundle_with_contents, strip_margin
 from conductr_cli import conduct_load
 from urllib.error import URLError
 import shutil
 
+try:
+    from unittest.mock import call, patch, MagicMock  # 3.3 and beyond
+except ImportError:
+    from mock import call, patch, MagicMock
+
 
 class TestConductLoadCommand(TestCase, CliTestCase):
 
diff --git a/conductr_cli/test/test_conduct_run.py b/conductr_cli/test/test_conduct_run.py
index 9f18628..2bdc26b 100644
--- a/conductr_cli/test/test_conduct_run.py
+++ b/conductr_cli/test/test_conduct_run.py
@@ -1,8 +1,12 @@
 from unittest import TestCase
-from unittest.mock import patch, MagicMock
 from conductr_cli.test.cli_test_case import CliTestCase, strip_margin
 from conductr_cli import conduct_run
 
+try:
+    from unittest.mock import patch, MagicMock  # 3.3 and beyond
+except ImportError:
+    from mock import patch, MagicMock
+
 
 class TestConductRunCommand(TestCase, CliTestCase):
 
diff --git a/conductr_cli/test/test_conduct_services.py b/conductr_cli/test/test_conduct_services.py
index 590794e..431afef 100644
--- a/conductr_cli/test/test_conduct_services.py
+++ b/conductr_cli/test/test_conduct_services.py
@@ -1,8 +1,12 @@
 from unittest import TestCase
-from unittest.mock import patch, MagicMock
 from conductr_cli.test.cli_test_case import CliTestCase, strip_margin
 from conductr_cli import conduct_services
 
+try:
+    from unittest.mock import patch, MagicMock  # 3.3 and beyond
+except ImportError:
+    from mock import patch, MagicMock
+
 
 class TestConductServicesCommand(TestCase, CliTestCase):
 
diff --git a/conductr_cli/test/test_conduct_stop.py b/conductr_cli/test/test_conduct_stop.py
index 2c3ab19..39a4e00 100644
--- a/conductr_cli/test/test_conduct_stop.py
+++ b/conductr_cli/test/test_conduct_stop.py
@@ -1,8 +1,12 @@
 from unittest import TestCase
-from unittest.mock import patch, MagicMock
 from conductr_cli.test.cli_test_case import CliTestCase, strip_margin
 from conductr_cli import conduct_stop
 
+try:
+    from unittest.mock import patch, MagicMock  # 3.3 and beyond
+except ImportError:
+    from mock import patch, MagicMock
+
 
 class TestConductStopCommand(TestCase, CliTestCase):
 
diff --git a/conductr_cli/test/test_conduct_unload.py b/conductr_cli/test/test_conduct_unload.py
index 1cbf7c2..bcff6ac 100644
--- a/conductr_cli/test/test_conduct_unload.py
+++ b/conductr_cli/test/test_conduct_unload.py
@@ -1,8 +1,12 @@
 from unittest import TestCase
-from unittest.mock import patch, MagicMock
 from conductr_cli.test.cli_test_case import CliTestCase, strip_margin
 from conductr_cli import conduct_unload
 
+try:
+    from unittest.mock import patch, MagicMock  # 3.3 and beyond
+except ImportError:
+    from mock import patch, MagicMock
+
 
 class TestConductUnloadCommand(TestCase, CliTestCase):
 
diff --git a/conductr_cli/test/test_shazar.py b/conductr_cli/test/test_shazar.py
index c8d0407..adfd475 100644
--- a/conductr_cli/test/test_shazar.py
+++ b/conductr_cli/test/test_shazar.py
@@ -1,7 +1,14 @@
 from unittest import TestCase
+import shutil
 import tempfile
 from os import remove
-from conductr_cli.shazar import create_digest, build_parser
+from conductr_cli.shazar import create_digest, build_parser, run
+from conductr_cli.test.cli_test_case import CliTestCase
+
+try:
+    from unittest.mock import patch, MagicMock  # 3.3 and beyond
+except ImportError:
+    from mock import patch, MagicMock
 
 
 class TestShazar(TestCase):
@@ -23,3 +30,25 @@ class TestShazar(TestCase):
 
         self.assertEqual(args.output_dir, 'output-dir')
         self.assertEqual(args.source, 'source')
+
+
+class TestIntegration(TestCase, CliTestCase):
+
+    def setUp(self):  # noqa
+        self.tmpdir = tempfile.mkdtemp()
+        self.tmpfile = tempfile.NamedTemporaryFile(mode='w+b', delete=False)
+        self.tmpfile.write(b'test file data')
+
+    def test(self):
+        stdout = MagicMock()
+        with patch('sys.stdout', stdout):
+            run('--output-dir {} {}'.format(self.tmpdir, self.tmpfile.name).split())
+
+        self.assertRegex(
+            self.output(stdout),
+            'Created digested ZIP archive at /tmp/tmp[a-z0-9_]{6,8}/tmp[a-z0-9_]{6,8}-[a-f0-9]{64}\.zip'
+        )
+
+    def tearDown(self):  # noqa
+        shutil.rmtree(self.tmpdir)
+        remove(self.tmpfile.name)

pip install -e .
parse_log_pytest
pytest
pytest
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
argcomplete==3.6.1
certifi==2025.1.31
charset-normalizer==3.4.1
-e git+https://github.com/typesafehub/conductr-cli.git@e5561a7e43d92a0c19e7b6e31a36448455a17fba#egg=conductr_cli
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
idna==3.10
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
packaging @ file:///croot/packaging_1734472117206/work
pluggy @ file:///croot/pluggy_1733169602837/work
pyhocon==0.2.1
pyparsing==2.0.3
pytest @ file:///croot/pytest_1738938843180/work
requests==2.32.3
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
urllib3==2.3.0

name: conductr-cli
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - argcomplete==3.6.1
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - idna==3.10
      - pyhocon==0.2.1
      - pyparsing==2.0.3
      - requests==2.32.3
      - urllib3==2.3.0
prefix: /opt/conda/envs/conductr-cli

conductr_cli/test/test_shazar.py::TestIntegration::test
conductr_cli/test/test_conduct_info.py::TestConductInfoCommand::test_double_digits conductr_cli/test/test_conduct_info.py::TestConductInfoCommand::test_failure_invalid_address conductr_cli/test/test_conduct_info.py::TestConductInfoCommand::test_has_error conductr_cli/test/test_conduct_info.py::TestConductInfoCommand::test_long_ids conductr_cli/test/test_conduct_info.py::TestConductInfoCommand::test_no_bundles conductr_cli/test/test_conduct_info.py::TestConductInfoCommand::test_one_running_one_starting_one_stopped conductr_cli/test/test_conduct_info.py::TestConductInfoCommand::test_one_running_one_stopped_verbose conductr_cli/test/test_conduct_info.py::TestConductInfoCommand::test_stopped_bundle conductr_cli/test/test_conduct_load.py::TestConductLoadCommand::test_failure conductr_cli/test/test_conduct_load.py::TestConductLoadCommand::test_failure_invalid_address conductr_cli/test/test_conduct_load.py::TestConductLoadCommand::test_failure_no_bundle conductr_cli/test/test_conduct_load.py::TestConductLoadCommand::test_failure_no_configuration conductr_cli/test/test_conduct_load.py::TestConductLoadCommand::test_failure_no_disk_space conductr_cli/test/test_conduct_load.py::TestConductLoadCommand::test_failure_no_memory conductr_cli/test/test_conduct_load.py::TestConductLoadCommand::test_failure_no_nr_of_cpus conductr_cli/test/test_conduct_load.py::TestConductLoadCommand::test_failure_no_roles conductr_cli/test/test_conduct_load.py::TestConductLoadCommand::test_failure_roles_not_a_list conductr_cli/test/test_conduct_load.py::TestConductLoadCommand::test_success conductr_cli/test/test_conduct_load.py::TestConductLoadCommand::test_success_custom_ip_port conductr_cli/test/test_conduct_load.py::TestConductLoadCommand::test_success_long_ids conductr_cli/test/test_conduct_load.py::TestConductLoadCommand::test_success_verbose conductr_cli/test/test_conduct_load.py::TestConductLoadCommand::test_success_with_configuration conductr_cli/test/test_conduct_run.py::TestConductRunCommand::test_failure conductr_cli/test/test_conduct_run.py::TestConductRunCommand::test_failure_invalid_address conductr_cli/test/test_conduct_run.py::TestConductRunCommand::test_success conductr_cli/test/test_conduct_run.py::TestConductRunCommand::test_success_long_ids conductr_cli/test/test_conduct_run.py::TestConductRunCommand::test_success_verbose conductr_cli/test/test_conduct_run.py::TestConductRunCommand::test_success_with_configuration conductr_cli/test/test_conduct_services.py::TestConductServicesCommand::test_no_bundles conductr_cli/test/test_conduct_services.py::TestConductServicesCommand::test_one_bundle_starting conductr_cli/test/test_conduct_services.py::TestConductServicesCommand::test_one_bundle_starting_long_ids conductr_cli/test/test_conduct_services.py::TestConductServicesCommand::test_two_bundles_mult_components_endpoints conductr_cli/test/test_conduct_services.py::TestConductServicesCommand::test_two_bundles_mult_components_endpoints_no_path conductr_cli/test/test_conduct_stop.py::TestConductStopCommand::test_failure conductr_cli/test/test_conduct_stop.py::TestConductStopCommand::test_failure_invalid_address conductr_cli/test/test_conduct_stop.py::TestConductStopCommand::test_success conductr_cli/test/test_conduct_stop.py::TestConductStopCommand::test_success_long_ids conductr_cli/test/test_conduct_stop.py::TestConductStopCommand::test_success_verbose conductr_cli/test/test_conduct_stop.py::TestConductStopCommand::test_success_with_configuration conductr_cli/test/test_conduct_unload.py::TestConductUnloadCommand::test_failure conductr_cli/test/test_conduct_unload.py::TestConductUnloadCommand::test_failure_invalid_address conductr_cli/test/test_conduct_unload.py::TestConductUnloadCommand::test_success conductr_cli/test/test_conduct_unload.py::TestConductUnloadCommand::test_success_verbose conductr_cli/test/test_conduct_unload.py::TestConductUnloadCommand::test_success_with_configuration conductr_cli/test/test_shazar.py::TestShazar::test_create_digest conductr_cli/test/test_shazar.py::TestShazar::test_parser_success
conductr_cli/shazar.py setup.py conductr_cli/conduct_logging.py .gitignore .travis.yml tox.ini
conductr_cli/shazar.py setup.py conductr_cli/conduct_logging.py .gitignore .travis.yml tox.ini