From 7c5b237e660714144b3c7914bc34a3fa9315ab9e Mon Sep 17 00:00:00 2001
From: Tomas Kopecek <tkopecek@redhat.com>
Date: Apr 22 2020 12:49:28 +0000
Subject: [PATCH 1/2] replace print + return 1 with error()


---

diff --git a/cli/koji_cli/commands.py b/cli/koji_cli/commands.py
index e85d137..0a8a88e 100644
--- a/cli/koji_cli/commands.py
+++ b/cli/koji_cli/commands.py
@@ -82,14 +82,12 @@ def handle_add_group(goptions, session, args):
 
     dsttag = session.getTag(tag)
     if not dsttag:
-        print("Unknown tag: %s" % tag)
-        return 1
+        error("Unknown tag: %s" % tag)
 
     groups = dict([(p['name'], p['group_id']) for p in session.getTagGroups(tag, inherit=False)])
     group_id = groups.get(group, None)
     if group_id is not None:
-        print("Group %s already exists for tag %s" % (group, tag))
-        return 1
+        error("Group %s already exists for tag %s" % (group, tag))
 
     session.groupListAdd(tag, group)
 
@@ -110,14 +108,12 @@ def handle_block_group(goptions, session, args):
 
     dsttag = session.getTag(tag)
     if not dsttag:
-        print("Unknown tag: %s" % tag)
-        return 1
+        error("Unknown tag: %s" % tag)
 
     groups = dict([(p['name'], p['group_id']) for p in session.getTagGroups(tag, inherit=False)])
     group_id = groups.get(group, None)
     if group_id is None:
-        print("Group %s doesn't exist within tag %s" % (group, tag))
-        return 1
+        error("Group %s doesn't exist within tag %s" % (group, tag))
 
     session.groupListBlock(tag, group)
 
@@ -234,15 +230,14 @@ def handle_edit_host(options, session, args):
     session.multicall = True
     for host in args:
         session.getHost(host)
-    error = False
+    error_hit = False
     for host, [info] in zip(args, session.multiCall(strict=True)):
         if not info:
-            print(_("Host %s does not exist") % host)
-            error = True
+            warn(_("Host %s does not exist") % host)
+            error_hit = True
 
-    if error:
-        print(_("No changes made, please correct the command line"))
-        return 1
+    if error_hit:
+        error(_("No changes made, please correct the command line"))
 
     session.multicall = True
     for host in args:
@@ -272,13 +267,11 @@ def handle_add_host_to_channel(goptions, session, args):
     if not options.new:
         channelinfo = session.getChannel(channel)
         if not channelinfo:
-            print("No such channel: %s" % channel)
-            return 1
+            error("No such channel: %s" % channel)
     host = args[0]
     hostinfo = session.getHost(host)
     if not hostinfo:
-        print("No such host: %s" % host)
-        return 1
+        error("No such host: %s" % host)
     kwargs = {}
     if options.new:
         kwargs['create'] = True
@@ -296,14 +289,12 @@ def handle_remove_host_from_channel(goptions, session, args):
     activate_session(session, goptions)
     hostinfo = session.getHost(host)
     if not hostinfo:
-        print("No such host: %s" % host)
-        return 1
+        error("No such host: %s" % host)
     hostchannels = [c['name'] for c in session.listChannels(hostinfo['id'])]
 
     channel = args[1]
     if channel not in hostchannels:
-        print("Host %s is not a member of channel %s" % (host, channel))
-        return 1
+        error("Host %s is not a member of channel %s" % (host, channel))
 
     session.removeHostFromChannel(host, channel)
 
@@ -319,8 +310,7 @@ def handle_remove_channel(goptions, session, args):
     activate_session(session, goptions)
     cinfo = session.getChannel(args[0])
     if not cinfo:
-        print("No such channel: %s" % args[0])
-        return 1
+        error("No such channel: %s" % args[0])
     session.removeChannel(args[0], force=options.force)
 
 
@@ -334,8 +324,7 @@ def handle_rename_channel(goptions, session, args):
     activate_session(session, goptions)
     cinfo = session.getChannel(args[0])
     if not cinfo:
-        print("No such channel: %s" % args[0])
-        return 1
+        error("No such channel: %s" % args[0])
     session.renameChannel(args[0], args[1])
 
 
@@ -352,8 +341,7 @@ def handle_add_pkg(goptions, session, args):
     if not options.owner:
         parser.error(_("Please specify an owner for the package(s)"))
     if not session.getUser(options.owner):
-        print("User %s does not exist" % options.owner)
-        return 1
+        error("User %s does not exist" % options.owner)
     activate_session(session, goptions)
     tag = args[0]
     opts = {}
@@ -398,18 +386,17 @@ def handle_block_pkg(goptions, session, args):
     # check if list of packages exists for that tag already
     dsttag = session.getTag(tag)
     if dsttag is None:
-        print("No such tag: %s" % tag)
-        return 1
+        error("No such tag: %s" % tag)
     pkglist = dict([(p['package_name'], p['package_id'])
                     for p in session.listPackages(tagID=dsttag['id'], inherited=True)])
     ret = 0
     for package in args[1:]:
         package_id = pkglist.get(package, None)
         if package_id is None:
-            print("Package %s doesn't exist in tag %s" % (package, tag))
+            warn("Package %s doesn't exist in tag %s" % (package, tag))
             ret = 1
     if ret:
-        return ret
+        error(code=ret)
     session.multicall = True
     for package in args[1:]:
         # force is not supported on older hub, so use it only explicitly
@@ -436,18 +423,17 @@ def handle_remove_pkg(goptions, session, args):
     # check if list of packages exists for that tag already
     dsttag = session.getTag(tag)
     if dsttag is None:
-        print("No such tag: %s" % tag)
-        return 1
+        error("No such tag: %s" % tag)
     pkglist = dict([(p['package_name'], p['package_id'])
                     for p in session.listPackages(tagID=dsttag['id'])])
     ret = 0
     for package in args[1:]:
         package_id = pkglist.get(package, None)
         if package_id is None:
-            print("Package %s is not in tag %s" % (package, tag))
+            warn("Package %s is not in tag %s" % (package, tag))
             ret = 1
     if ret:
-        return ret
+        error(code=ret)
     session.multicall = True
     for package in args[1:]:
         session.packageListRemove(tag, package, **opts)
@@ -584,11 +570,9 @@ def handle_chain_build(options, session, args):
     ancestors = session.getFullInheritance(build_target['build_tag'])
     if dest_tag['id'] not in [build_target['build_tag']] + \
             [ancestor['parent_id'] for ancestor in ancestors]:
-        print(_("Packages in destination tag %(dest_tag_name)s are not inherited by build tag "
-                "%(build_tag_name)s" % build_target))
-        print(_("Target %s is not usable for a chain-build" % build_target['name']))
-        return 1
-
+        warn(_("Packages in destination tag %(dest_tag_name)s are not inherited by build tag "
+               "%(build_tag_name)s" % build_target))
+        error(_("Target %s is not usable for a chain-build" % build_target['name']))
     sources = args[1:]
 
     src_list = []
@@ -608,8 +592,7 @@ def handle_chain_build(options, session, args):
             # quick check that it looks like a N-V-R
             build_level.append(src)
         else:
-            print(_('"%s" is not a SCM URL or package N-V-R' % src))
-            return 1
+            error(_('"%s" is not a SCM URL or package N-V-R' % src))
     if build_level:
         src_list.append(build_level)
 
@@ -980,8 +963,7 @@ def anon_handle_mock_config(goptions, session, args):
             parser.error(_("Task id must be an integer"))
         broots = session.listBuildroots(taskID=task_id)
         if not broots:
-            print(_("No buildroots for task %s (or no such task)") % options.task)
-            return 1
+            error(_("No buildroots for task %s (or no such task)") % options.task)
         if len(broots) > 1:
             print(_("Multiple buildroots found: %s" % [br['id'] for br in broots]))
         brootinfo = broots[-1]
@@ -995,29 +977,25 @@ def anon_handle_mock_config(goptions, session, args):
             options.name = "%s-task_%i" % (opts['tag_name'], task_id)
     elif options.tag:
         if not options.arch:
-            print(_("Please specify an arch"))
-            return 1
+            error(_("Please specify an arch"))
         tag = session.getTag(options.tag)
         if not tag:
             parser.error(_("Invalid tag: %s" % options.tag))
         arch = options.arch
         config = session.getBuildConfig(tag['id'])
         if not config:
-            print(_("Could not get config info for tag: %(name)s") % tag)
-            return 1
+            error(_("Could not get config info for tag: %(name)s") % tag)
         opts['tag_name'] = tag['name']
         if options.latest:
             opts['repoid'] = 'latest'
         else:
             repo = session.getRepo(config['id'])
             if not repo:
-                print(_("Could not get a repo for tag: %(name)s") % tag)
-                return 1
+                error(_("Could not get a repo for tag: %(name)s") % tag)
             opts['repoid'] = repo['id']
     elif options.target:
         if not options.arch:
-            print(_("Please specify an arch"))
-            return 1
+            error(_("Please specify an arch"))
         arch = options.arch
         target = session.getBuildTarget(options.target)
         if not target:
@@ -1028,8 +1006,7 @@ def anon_handle_mock_config(goptions, session, args):
         else:
             repo = session.getRepo(target['build_tag'])
             if not repo:
-                print(_("Could not get a repo for tag: %s") % opts['tag_name'])
-                return 1
+                error(_("Could not get a repo for tag: %s") % opts['tag_name'])
             opts['repoid'] = repo['id']
     else:
         parser.error(_("Please specify one of: --tag, --target, --task, --buildroot"))
@@ -1069,14 +1046,13 @@ def handle_disable_host(goptions, session, args):
     session.multicall = True
     for host in args:
         session.getHost(host)
-    error = False
+    error_hit = False
     for host, [id] in zip(args, session.multiCall(strict=True)):
         if not id:
             print("Host %s does not exist" % host)
-            error = True
-    if error:
-        print("No changes made. Please correct the command line.")
-        return 1
+            error_hit = True
+    if error_hit:
+        error("No changes made. Please correct the command line.")
     session.multicall = True
     for host in args:
         session.disableHost(host)
@@ -1099,14 +1075,13 @@ def handle_enable_host(goptions, session, args):
     session.multicall = True
     for host in args:
         session.getHost(host)
-    error = False
+    error_hit = False
     for host, [id] in zip(args, session.multiCall(strict=True)):
         if not id:
             print("Host %s does not exist" % host)
-            error = True
-    if error:
-        print("No changes made. Please correct the command line.")
-        return 1
+            error_hit = True
+    if error_hit:
+        error("No changes made. Please correct the command line.")
     session.multicall = True
     for host in args:
         session.enableHost(host)
@@ -1149,10 +1124,9 @@ def handle_restart_hosts(options, session, args):
         }
         others = session.listTasks(query)
         if others:
-            print('Found other restartHosts tasks running.')
-            print('Task ids: %r' % [t['id'] for t in others])
-            print('Use --force to run anyway')
-            return 1
+            warn('Found other restartHosts tasks running.')
+            warn('Task ids: %r' % [t['id'] for t in others])
+            error('Use --force to run anyway')
 
     callopts = {}
     if my_opts.channel:
@@ -1389,15 +1363,13 @@ def handle_import_comps(goptions, session, args):
     # check if the tag exists
     dsttag = session.getTag(args[1])
     if dsttag is None:
-        print("No such tag: %s" % args[1])
-        return 1
+        error("No such tag: %s" % args[1])
     if libcomps is not None:
         _import_comps(session, args[0], args[1], local_options)
     elif yumcomps is not None:
         _import_comps_alt(session, args[0], args[1], local_options)
     else:
-        print("comps module not available")
-        return 1
+        error("comps module not available")
 
 
 def _import_comps(session, filename, tag, options):
@@ -1933,8 +1905,7 @@ def handle_set_build_volume(goptions, session, args):
         parser.error(_("You must provide a volume and at least one build"))
     volinfo = session.getVolume(args[0])
     if not volinfo:
-        print("No such volume: %s" % args[0])
-        return 1
+        error("No such volume: %s" % args[0])
     activate_session(session, goptions)
     builds = []
     for nvr in args[1:]:
@@ -1946,8 +1917,7 @@ def handle_set_build_volume(goptions, session, args):
         else:
             builds.append(binfo)
     if not builds:
-        print("No builds to move")
-        return 1
+        error("No builds to move")
     for binfo in builds:
         session.changeBuildVolume(binfo['id'], volinfo['id'])
         if options.verbose:
@@ -1964,8 +1934,7 @@ def handle_add_volume(goptions, session, args):
     name = args[0]
     volinfo = session.getVolume(name)
     if volinfo:
-        print("Volume %s already exists" % name)
-        return 1
+        error("Volume %s already exists" % name)
     activate_session(session, goptions)
     volinfo = session.addVolume(name)
     print("Added volume %(name)s with id %(id)i" % volinfo)
@@ -1993,8 +1962,7 @@ def handle_list_permissions(goptions, session, args):
     if options.user:
         user = session.getUser(options.user)
         if not user:
-            print("User %s does not exist" % options.user)
-            return 1
+            error("User %s does not exist" % options.user)
         perms = session.getUserPerms(user['id'])
     elif options.mine:
         perms = session.getPerms()
@@ -2987,8 +2955,7 @@ def anon_handle_list_pkgs(goptions, session, args):
 
     data = session.listPackages(**opts)
     if not data:
-        print("(no matching packages)")
-        return 1
+        error("(no matching packages)")
     if not options.quiet:
         if allpkgs:
             print("Package")
@@ -3893,14 +3860,11 @@ def handle_add_target(goptions, session, args):
     chkbuildtag = session.getTag(build_tag)
     chkdesttag = session.getTag(dest_tag)
     if not chkbuildtag:
-        print("Build tag does not exist: %s" % build_tag)
-        return 1
+        error("Build tag does not exist: %s" % build_tag)
     if not chkbuildtag.get("arches", None):
-        print("Build tag has no arches: %s" % build_tag)
-        return 1
+        error("Build tag has no arches: %s" % build_tag)
     if not chkdesttag:
-        print("Destination tag does not exist: %s" % dest_tag)
-        return 1
+        error("Destination tag does not exist: %s" % dest_tag)
 
     session.createBuildTarget(name, build_tag, dest_tag)
 
@@ -3934,16 +3898,13 @@ def handle_edit_target(goptions, session, args):
         targetInfo['build_tag_name'] = options.build_tag
         chkbuildtag = session.getTag(options.build_tag)
         if not chkbuildtag:
-            print("Build tag does not exist: %s" % options.build_tag)
-            return 1
+            error("Build tag does not exist: %s" % options.build_tag)
         if not chkbuildtag.get("arches", None):
-            print("Build tag has no arches: %s" % options.build_tag)
-            return 1
+            error("Build tag has no arches: %s" % options.build_tag)
     if options.dest_tag:
         chkdesttag = session.getTag(options.dest_tag)
         if not chkdesttag:
-            print("Destination tag does not exist: %s" % options.dest_tag)
-            return 1
+            error("Destination tag does not exist: %s" % options.dest_tag)
         targetInfo['dest_tag_name'] = options.dest_tag
 
     session.editBuildTarget(targetInfo['orig_name'], targetInfo['name'],
@@ -3966,8 +3927,7 @@ def handle_remove_target(goptions, session, args):
     target = args[0]
     target_info = session.getBuildTarget(target)
     if not target_info:
-        print("Build target %s does not exist" % target)
-        return 1
+        error("Build target %s does not exist" % target)
 
     session.deleteBuildTarget(target_info['id'])
 
@@ -3988,8 +3948,7 @@ def handle_remove_tag(goptions, session, args):
     tag = args[0]
     tag_info = session.getTag(tag)
     if not tag_info:
-        print("Tag %s does not exist" % tag)
-        return 1
+        error("Tag %s does not exist" % tag)
 
     session.deleteTag(tag_info['id'])
 
@@ -5260,18 +5219,14 @@ def handle_edit_tag_inheritance(goptions, session, args):
         data = [datum for datum in data if datum['priority'] == priority]
 
     if len(data) == 0:
-        print(_("No inheritance link found to remove.  Please check your arguments"))
-        return 1
+        error(_("No inheritance link found to remove.  Please check your arguments"))
     elif len(data) > 1:
         print(_("Multiple matches for tag."))
         if not parent:
-            print(_("Please specify a parent on the command line."))
-            return 1
+            error(_("Please specify a parent on the command line."))
         if not priority:
-            print(_("Please specify a priority on the command line."))
-            return 1
-        print(_("Error: Key constraints may be broken.  Exiting."))
-        return 1
+            error(_("Please specify a priority on the command line."))
+        error(_("Error: Key constraints may be broken.  Exiting."))
 
     # len(data) == 1
     data = data[0]
@@ -5279,9 +5234,8 @@ def handle_edit_tag_inheritance(goptions, session, args):
     inheritanceData = session.getInheritanceData(tag['id'])
     samePriority = [datum for datum in inheritanceData if datum['priority'] == options.priority]
     if samePriority:
-        print(_("Error: There is already an active inheritance with that priority on %s, "
+        error(_("Error: There is already an active inheritance with that priority on %s, "
                 "please specify a different priority with --priority.") % tag['name'])
-        return 1
 
     new_data = data.copy()
     if options.priority is not None and options.priority.isdigit():
@@ -5292,8 +5246,7 @@ def handle_edit_tag_inheritance(goptions, session, args):
         elif options.maxdepth.lower() == "none":
             new_data['maxdepth'] = None
         else:
-            print(_("Invalid maxdepth: %s") % options.maxdepth)
-            return 1
+            error(_("Invalid maxdepth: %s") % options.maxdepth)
     if options.intransitive:
         new_data['intransitive'] = options.intransitive
     if options.noconfig:
@@ -5616,10 +5569,9 @@ def handle_remove_external_repo(goptions, session, args):
     if delete:
         # removing entirely
         if current_tags and not options.force:
-            print(_("Error: external repo %s used by tag(s): %s") %
-                  (repo, ', '.join(current_tags)))
-            print(_("Use --force to remove anyway"))
-            return 1
+            warn(_("Error: external repo %s used by tag(s): %s") %
+                 (repo, ', '.join(current_tags)))
+            error(_("Use --force to remove anyway"))
         session.deleteExternalRepo(args[0])
     else:
         for tag in tags:
@@ -6441,15 +6393,13 @@ def handle_set_pkg_owner_global(goptions, session, args):
     packages = args[1:]
     user = session.getUser(owner)
     if not user:
-        print("No such user: %s" % owner)
-        return 1
+        error("No such user: %s" % owner)
     opts = {'with_dups': True}
     old_user = None
     if options.old_user:
         old_user = session.getUser(options.old_user)
         if not old_user:
-            print("No such user: %s" % options.old_user)
-            return 1
+            error("No such user: %s" % options.old_user)
         opts['userID'] = old_user['id']
     to_change = []
     for package in packages:
@@ -6461,8 +6411,7 @@ def handle_set_pkg_owner_global(goptions, session, args):
     if not packages and options.old_user:
         entries = session.listPackages(**opts)
         if not entries:
-            print("No data for user %s" % old_user['name'])
-            return 1
+            error("No data for user %s" % old_user['name'])
         to_change.extend(entries)
     for entry in to_change:
         if user['id'] == entry['owner_id']:
@@ -6730,11 +6679,11 @@ def handle_untag_build(goptions, session, args):
                 # not in tag, see if it even exists
                 binfo = session.getBuild(nvr)
                 if not binfo:
-                    print(_("No such build: %s") % nvr)
+                    warn(_("No such build: %s") % nvr)
                 else:
-                    print(_("Build %s not in tag %s") % (nvr, tag['name']))
+                    warn(_("Build %s not in tag %s") % (nvr, tag['name']))
                 if not options.force:
-                    return 1
+                    error()
     builds.reverse()
     for binfo in builds:
         if options.test:
@@ -6791,7 +6740,7 @@ def anon_handle_download_build(options, session, args):
 
     if build.isdigit():
         if suboptions.latestfrom:
-            error("--latestfrom not compatible with build IDs, specify a package name.")
+            parser.error("--latestfrom not compatible with build IDs, specify a package name.")
         build = int(build)
         if suboptions.task_id:
             builds = session.listBuilds(taskID=build)
@@ -7115,13 +7064,13 @@ def anon_handle_wait_repo(options, session, args):
             parser.error(_("Invalid tag: %s") % tag)
         targets = session.getBuildTargets(buildTagID=tag_info['id'])
         if not targets:
-            print("%(name)s is not a build tag for any target" % tag_info)
+            warn("%(name)s is not a build tag for any target" % tag_info)
             targets = session.getBuildTargets(destTagID=tag_info['id'])
             if targets:
                 maybe = {}.fromkeys([t['build_tag_name'] for t in targets])
                 maybe = sorted(maybe.keys())
-                print("Suggested tags: %s" % ', '.join(maybe))
-            return 1
+                warn("Suggested tags: %s" % ', '.join(maybe))
+            error()
         tag_id = tag_info['id']
 
     for nvr in builds:
@@ -7150,12 +7099,12 @@ def anon_handle_wait_repo(options, session, args):
         if (time.time() - start) >= (suboptions.timeout * 60.0):
             if not suboptions.quiet:
                 if builds:
-                    print("Unsuccessfully waited %s for %s to appear in the %s repo" %
+                    error("Unsuccessfully waited %s for %s to appear in the %s repo" %
                           (koji.util.duration(start), koji.util.printList(suboptions.builds), tag))
                 else:
-                    print("Unsuccessfully waited %s for a new %s repo" %
+                    error("Unsuccessfully waited %s for a new %s repo" %
                           (koji.util.duration(start), tag))
-            return 1
+            error()
 
         time.sleep(options.poll_interval)
         last_repo = repo
@@ -7439,8 +7388,7 @@ def anon_handle_list_notifications(goptions, session, args):
     if options.user:
         user = session.getUser(options.user)
         if not user:
-            print("User %s does not exist" % options.user)
-            return 1
+            error("User %s does not exist" % options.user)
         user_id = user['id']
     else:
         user_id = None

From 2372b3c2b4602e9dd8570765dfdc1d9c1e7e2acc Mon Sep 17 00:00:00 2001
From: Tomas Kopecek <tkopecek@redhat.com>
Date: Apr 22 2020 12:49:28 +0000
Subject: [PATCH 2/2] fix tests


---

diff --git a/tests/test_cli/test_add_group.py b/tests/test_cli/test_add_group.py
index a045647..dc1582c 100644
--- a/tests/test_cli/test_add_group.py
+++ b/tests/test_cli/test_add_group.py
@@ -51,9 +51,9 @@ class TestAddGroup(utils.CliTestCase):
         session.groupListAdd.assert_called_once_with(tag, group)
         self.assertNotEqual(rv, 1)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
-    def test_handle_add_group_dupl(self, activate_session_mock, stdout):
+    def test_handle_add_group_dupl(self, activate_session_mock, stderr):
         tag = 'tag'
         group = 'group'
         arguments = [tag, group]
@@ -67,8 +67,10 @@ class TestAddGroup(utils.CliTestCase):
             {'name': 'group', 'group_id': 'groupId'}]
 
         # Run it and check immediate output
-        rv = handle_add_group(options, session, arguments)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_add_group(options, session, arguments)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = 'Group group already exists for tag tag\n'
         self.assertMultiLineEqual(actual, expected)
 
@@ -78,7 +80,6 @@ class TestAddGroup(utils.CliTestCase):
         session.getTag.assert_called_once_with(tag)
         session.getTagGroups.assert_called_once_with(tag, inherit=False)
         session.groupListAdd.assert_not_called()
-        self.assertEqual(rv, 1)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
@@ -139,9 +140,9 @@ class TestAddGroup(utils.CliTestCase):
         session.getTagGroups.assert_not_called()
         session.groupListAdd.assert_not_called()
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
-    def test_handle_add_group_no_tag(self, activate_session_mock, stdout):
+    def test_handle_add_group_no_tag(self, activate_session_mock, stderr):
         tag = 'tag'
         group = 'group'
         arguments = [tag, group]
@@ -153,8 +154,10 @@ class TestAddGroup(utils.CliTestCase):
         session.getTag.return_value = None
 
         # Run it and check immediate output
-        rv = handle_add_group(options, session, arguments)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_add_group(options, session, arguments)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = 'Unknown tag: tag\n'
         self.assertMultiLineEqual(actual, expected)
 
@@ -164,7 +167,6 @@ class TestAddGroup(utils.CliTestCase):
         session.getTag.assert_called_once_with(tag)
         session.getTagGroups.assert_not_called()
         session.groupListAdd.assert_not_called()
-        self.assertEqual(rv, 1)
 
 
 if __name__ == '__main__':
diff --git a/tests/test_cli/test_add_host.py b/tests/test_cli/test_add_host.py
index 5c8faa7..511e46e 100644
--- a/tests/test_cli/test_add_host.py
+++ b/tests/test_cli/test_add_host.py
@@ -3,15 +3,12 @@ import mock
 import os
 import six
 import sys
-try:
-    import unittest2 as unittest
-except ImportError:
-    import unittest
 
 import koji
 from koji_cli.commands import handle_add_host
+from . import utils
 
-class TestAddHost(unittest.TestCase):
+class TestAddHost(utils.CliTestCase):
 
     # Show long diffs in error output...
     maxDiff = None
@@ -90,8 +87,9 @@ class TestAddHost(unittest.TestCase):
         # Run it and check immediate output
         # args: host, arch1, arch2, --krb-principal=krb
         # expected: failed, host already exists
-        with self.assertRaises(SystemExit):
+        with self.assertRaises(SystemExit) as ex:
             handle_add_host(options, session, arguments)
+        self.assertExitCode(ex, 1)
         actual = stderr.getvalue()
         expected = 'host is already in the database\n'
         self.assertMultiLineEqual(actual, expected)
@@ -112,8 +110,9 @@ class TestAddHost(unittest.TestCase):
         session = mock.MagicMock()
 
         # Run it and check immediate output
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_add_host(options, session, arguments)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -129,14 +128,10 @@ class TestAddHost(unittest.TestCase):
         activate_session_mock.assert_not_called()
         session.hasHost.assert_not_called()
         session.addHost.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
-    def test_handle_add_host_failed(self, activate_session_mock, stdout):
+    def test_handle_add_host_failed(self, activate_session_mock, stderr):
         host = 'host'
         arches = ['arch1', 'arch2']
         krb_principal = '--krb-principal=krb'
@@ -153,16 +148,12 @@ class TestAddHost(unittest.TestCase):
         # Run it and check immediate output
         # args: host, arch1, arch2, --krb-principal=krb
         # expected: failed
-        with self.assertRaises(koji.GenericError):
+        with self.assertRaises(koji.GenericError) as ex:
             handle_add_host(options, session, arguments)
-        actual = stdout.getvalue()
+        actual = stderr.getvalue()
         expected = ''
         self.assertMultiLineEqual(actual, expected)
         # Finally, assert that things were called as we expected.
         activate_session_mock.assert_called_once_with(session, options)
         session.getHost.assert_called_once_with(host)
         session.addHost.assert_called_once_with(host, arches, **kwargs)
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/tests/test_cli/test_add_host_to_channel.py b/tests/test_cli/test_add_host_to_channel.py
index 9220bfe..1c3c245 100644
--- a/tests/test_cli/test_add_host_to_channel.py
+++ b/tests/test_cli/test_add_host_to_channel.py
@@ -10,8 +10,9 @@ except ImportError:
     import unittest
 
 from koji_cli.commands import handle_add_host_to_channel
+from . import utils
 
-class TestAddHostToChannel(unittest.TestCase):
+class TestAddHostToChannel(utils.CliTestCase):
 
     # Show long diffs in error output...
     maxDiff = None
@@ -104,10 +105,10 @@ class TestAddHostToChannel(unittest.TestCase):
             host, channel, **kwargs)
         self.assertNotEqual(rv, 1)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
     def test_handle_add_host_to_channel_no_channel(
-            self, activate_session_mock, stdout):
+            self, activate_session_mock, stderr):
         host = 'host'
         channel = 'channel'
         channel_info = None
@@ -121,8 +122,10 @@ class TestAddHostToChannel(unittest.TestCase):
         # Run it and check immediate output
         # args: host, channel
         # expected: failed, channel not found
-        rv = handle_add_host_to_channel(options, session, args)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_add_host_to_channel(options, session, args)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = 'No such channel: channel\n'
         self.assertMultiLineEqual(actual, expected)
         # Finally, assert that things were called as we expected.
@@ -130,12 +133,11 @@ class TestAddHostToChannel(unittest.TestCase):
         session.getChannel.assert_called_once_with(channel)
         session.getHost.assert_not_called()
         session.addHostToChannel.assert_not_called()
-        self.assertEqual(rv, 1)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
     def test_handle_add_host_to_channel_no_host(
-            self, activate_session_mock, stdout):
+            self, activate_session_mock, stderr):
         host = 'host'
         host_info = None
         channel = 'channel'
@@ -151,8 +153,10 @@ class TestAddHostToChannel(unittest.TestCase):
         # Run it and check immediate output
         # args: host, channel
         # expected: success
-        rv = handle_add_host_to_channel(options, session, args)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_add_host_to_channel(options, session, args)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = 'No such host: host\n'
         self.assertMultiLineEqual(actual, expected)
         # Finally, assert that things were called as we expected.
@@ -160,7 +164,6 @@ class TestAddHostToChannel(unittest.TestCase):
         session.getChannel.assert_called_once_with(channel)
         session.getHost.assert_called_once_with(host)
         session.addHostToChannel.assert_not_called()
-        self.assertEqual(rv, 1)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
@@ -177,8 +180,9 @@ class TestAddHostToChannel(unittest.TestCase):
         # Run it and check immediate output
         # args: _empty_
         # expected: failed, help msg shows
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_add_host_to_channel(options, session, args)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -196,10 +200,6 @@ class TestAddHostToChannel(unittest.TestCase):
         session.getChannel.assert_not_called()
         session.listChannels.assert_not_called()
         session.addHostToChannel.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
 
 if __name__ == '__main__':
diff --git a/tests/test_cli/test_add_pkg.py b/tests/test_cli/test_add_pkg.py
index 20bca2a..e8d5b28 100644
--- a/tests/test_cli/test_add_pkg.py
+++ b/tests/test_cli/test_add_pkg.py
@@ -12,9 +12,10 @@ except ImportError:
 from mock import call
 
 from koji_cli.commands import handle_add_pkg
+from . import utils
 
 
-class TestAddPkg(unittest.TestCase):
+class TestAddPkg(utils.CliTestCase):
 
     # Show long diffs in error output...
     maxDiff = None
@@ -108,10 +109,10 @@ class TestAddPkg(unittest.TestCase):
                           call.multiCall(strict=True)])
         self.assertNotEqual(rv, 1)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
     def test_handle_add_pkg_owner_no_exists(
-            self, activate_session_mock, stdout):
+            self, activate_session_mock, stderr):
         tag = 'tag'
         packages = ['package1', 'package2', 'package3']
         owner = 'owner'
@@ -131,15 +132,16 @@ class TestAddPkg(unittest.TestCase):
         # args: --owner, --extra-arches='arch1,arch2 arch3, arch4',
         #       tag, package1, package2, package3
         # expected: failed: owner does not exist
-        rv = handle_add_pkg(options, session, args)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_add_pkg(options, session, args)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = 'User owner does not exist\n'
         self.assertMultiLineEqual(actual, expected)
         # Finally, assert that things were called as we expected.
         activate_session_mock.assert_not_called()
         self.assertEqual(session.mock_calls,
                          [call.getUser(owner)])
-        self.assertEqual(rv, 1)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
@@ -165,8 +167,9 @@ class TestAddPkg(unittest.TestCase):
         # args: --owner, --extra-arches='arch1,arch2 arch3, arch4',
         #       tag, package1, package2, package3
         # expected: failed: tag does not exist
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_add_pkg(options, session, args)
+        self.assertExitCode(ex, 1)
         actual = stdout.getvalue()
         expected = 'No such tag: tag\n'
         self.assertMultiLineEqual(actual, expected)
@@ -175,10 +178,6 @@ class TestAddPkg(unittest.TestCase):
         self.assertEqual(session.mock_calls,
                          [call.getUser(owner),
                           call.getTag(tag)])
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 1)
-        else:
-            self.assertEqual(cm.exception.code, 1)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
@@ -197,8 +196,9 @@ class TestAddPkg(unittest.TestCase):
         session = mock.MagicMock()
 
         # Run it and check immediate output
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_add_pkg(options, session, args)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -216,10 +216,6 @@ class TestAddPkg(unittest.TestCase):
         session.getTag.assert_not_called()
         session.listPackages.assert_not_called()
         session.packageListAdd.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
@@ -234,8 +230,9 @@ class TestAddPkg(unittest.TestCase):
         session = mock.MagicMock()
 
         # Run it and check immediate output
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_add_pkg(options, session, args)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -253,10 +250,6 @@ class TestAddPkg(unittest.TestCase):
         session.getTag.assert_not_called()
         session.listPackages.assert_not_called()
         session.packageListAdd.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
 
 if __name__ == '__main__':
diff --git a/tests/test_cli/test_add_volume.py b/tests/test_cli/test_add_volume.py
index 4a0b889..0710321 100644
--- a/tests/test_cli/test_add_volume.py
+++ b/tests/test_cli/test_add_volume.py
@@ -22,12 +22,14 @@ class TestAddVolume(utils.CliTestCase):
 %s: error: {message}
 """ % (self.progname, self.progname)
 
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
     def test_handle_add_volume(
             self,
             activate_session_mock,
-            stdout):
+            stdout,
+            stderr):
         """Test handle_add_volume function"""
         session = mock.MagicMock()
         options = mock.MagicMock()
@@ -49,8 +51,10 @@ class TestAddVolume(utils.CliTestCase):
         # Case 2. volume already exists
         expected = "Volume %s already exists" % vol_name + "\n"
         session.getVolume.return_value = vol_info
-        self.assertEqual(1, handle_add_volume(options, session, [vol_name]))
-        self.assert_console_message(stdout, expected)
+        with self.assertRaises(SystemExit) as ex:
+            handle_add_volume(options, session, [vol_name])
+        self.assertExitCode(ex, 1)
+        self.assert_console_message(stderr, expected)
         session.getVolume.assert_called_with(vol_name)
         activate_session_mock.assert_not_called()
 
diff --git a/tests/test_cli/test_block_group.py b/tests/test_cli/test_block_group.py
index de83739..a626a66 100644
--- a/tests/test_cli/test_block_group.py
+++ b/tests/test_cli/test_block_group.py
@@ -18,9 +18,9 @@ class TestBlockGroup(utils.CliTestCase):
 %s: error: {message}
 """ % (self.progname, self.progname)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
-    def test_handle_block_group_nonexistent_tag(self, activate_session_mock, stdout):
+    def test_handle_block_group_nonexistent_tag(self, activate_session_mock, stderr):
         tag = 'nonexistent-tag'
         group = 'group'
         arguments = [tag, group]
@@ -32,8 +32,10 @@ class TestBlockGroup(utils.CliTestCase):
         session.getTag.return_value = None
 
         # Run it and check immediate output
-        rv = handle_block_group(options, session, arguments)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_block_group(options, session, arguments)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = 'Unknown tag: %s\n' % tag
         self.assertMultiLineEqual(actual, expected)
 
@@ -43,11 +45,10 @@ class TestBlockGroup(utils.CliTestCase):
         session.getTag.assert_called_once_with(tag)
         session.getTagGroups.assert_not_called()
         session.groupListBlock.assert_not_called()
-        self.assertEqual(rv, 1)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
-    def test_handle_block_group_nonexistent_group(self, activate_session_mock, stdout):
+    def test_handle_block_group_nonexistent_group(self, activate_session_mock, stderr):
         tag = 'tag'
         group = 'group'
         arguments = [tag, group]
@@ -60,8 +61,10 @@ class TestBlockGroup(utils.CliTestCase):
         session.getTagGroups.return_value = []
 
         # Run it and check immediate output
-        rv = handle_block_group(options, session, arguments)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_block_group(options, session, arguments)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = "Group %s doesn't exist within tag %s\n" % (group, tag)
         self.assertMultiLineEqual(actual, expected)
 
@@ -71,7 +74,6 @@ class TestBlockGroup(utils.CliTestCase):
         session.getTag.assert_called_once_with(tag)
         session.getTagGroups.assert_called_once_with(tag, inherit=False)
         session.groupListBlock.assert_not_called()
-        self.assertEqual(rv, 1)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
diff --git a/tests/test_cli/test_block_pkg.py b/tests/test_cli/test_block_pkg.py
index a7bf3e0..3a906ca 100644
--- a/tests/test_cli/test_block_pkg.py
+++ b/tests/test_cli/test_block_pkg.py
@@ -3,17 +3,14 @@ import mock
 import os
 import six
 import sys
-try:
-    import unittest2 as unittest
-except ImportError:
-    import unittest
 
 from mock import call
 
 from koji_cli.commands import handle_block_pkg
+from . import utils
 
 
-class TestBlockPkg(unittest.TestCase):
+class TestBlockPkg(utils.CliTestCase):
 
     # Show long diffs in error output...
     maxDiff = None
@@ -48,7 +45,7 @@ class TestBlockPkg(unittest.TestCase):
         session.packageListBlock.assert_called_once_with(
             tag, package, force=True)
         session.multiCall.assert_called_once_with(strict=True)
-        self.assertNotEqual(rv, 1)
+        self.assertFalse(rv)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
@@ -88,9 +85,9 @@ class TestBlockPkg(unittest.TestCase):
                 call.multiCall(strict=True)])
         self.assertNotEqual(rv, 1)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
-    def test_handle_block_pkg_no_package(self, activate_session_mock, stdout):
+    def test_handle_block_pkg_no_package(self, activate_session_mock, stderr):
         tag = 'tag'
         dsttag = {'name': tag, 'id': 1}
         packages = ['package1', 'package2', 'package3']
@@ -108,8 +105,10 @@ class TestBlockPkg(unittest.TestCase):
         # Run it and check immediate output
         # args: tag, package1, package2, package3
         # expected: failed: can not find package2 under tag
-        rv = handle_block_pkg(options, session, args)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_block_pkg(options, session, args)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = 'Package package2 doesn\'t exist in tag tag\n'
         self.assertMultiLineEqual(actual, expected)
         # Finally, assert that things were called as we expected.
@@ -119,12 +118,11 @@ class TestBlockPkg(unittest.TestCase):
             tagID=dsttag['id'], inherited=True)
         session.packageListBlock.assert_not_called()
         session.multiCall.assert_not_called()
-        self.assertEqual(rv, 1)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
     def test_handle_block_pkg_tag_no_exists(
-            self, activate_session_mock, stdout):
+            self, activate_session_mock, stderr):
         tag = 'tag'
         dsttag = None
         packages = ['package1', 'package2', 'package3']
@@ -138,8 +136,10 @@ class TestBlockPkg(unittest.TestCase):
         # Run it and check immediate output
         # args: tag, package1, package2, package3
         # expected: failed: tag does not exist
-        rv = handle_block_pkg(options, session, args)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_block_pkg(options, session, args)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = 'No such tag: tag\n'
         self.assertMultiLineEqual(actual, expected)
         # Finally, assert that things were called as we expected.
@@ -147,7 +147,6 @@ class TestBlockPkg(unittest.TestCase):
         session.getTag.assert_called_once_with(tag)
         session.listPackages.assert_not_called()
         session.packageListBlock.assert_not_called()
-        self.assertEqual(rv, 1)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
@@ -163,8 +162,9 @@ class TestBlockPkg(unittest.TestCase):
         session = mock.MagicMock()
 
         # Run it and check immediate output
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_block_pkg(options, session, args)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -181,11 +181,3 @@ class TestBlockPkg(unittest.TestCase):
         session.getTag.assert_not_called()
         session.listPackages.assert_not_called()
         session.packageListBlock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/tests/test_cli/test_build.py b/tests/test_cli/test_build.py
index 0766916..3c6477a 100644
--- a/tests/test_cli/test_build.py
+++ b/tests/test_cli/test_build.py
@@ -152,8 +152,9 @@ Task info: weburl/taskinfo?taskID=1
         progname = os.path.basename(sys.argv[0]) or 'koji'
 
         # Run it and check immediate output
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_build(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -171,10 +172,6 @@ Task info: weburl/taskinfo?taskID=1
         self.session.build.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
@@ -194,8 +191,9 @@ Task info: weburl/taskinfo?taskID=1
         progname = os.path.basename(sys.argv[0]) or 'koji'
 
         # Run it and check immediate output
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_build(self.options, self.session, args)
+        self.assertExitCode(ex, 0)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = """Usage: %s build [options] <target> <srpm path or scm url>
@@ -242,10 +240,6 @@ Options:
         self.session.build.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 0)
-        else:
-            self.assertEqual(cm.exception.code, 0)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
@@ -268,8 +262,9 @@ Options:
         progname = os.path.basename(sys.argv[0]) or 'koji'
 
         # Run it and check immediate output
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_build(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -287,10 +282,6 @@ Options:
         self.session.build.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
@@ -361,8 +352,9 @@ Task info: weburl/taskinfo?taskID=1
         # Run it and check immediate output
         # args: target, http://scm
         # expected: failed, target not found
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_build(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual = stderr.getvalue()
         expected = self.format_error_message( "Unknown build target: target")
         self.assertMultiLineEqual(actual, expected)
@@ -376,10 +368,6 @@ Task info: weburl/taskinfo?taskID=1
         self.session.build.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
     @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
@@ -408,8 +396,9 @@ Task info: weburl/taskinfo?taskID=1
         # Run it and check immediate output
         # args: target, http://scm
         # expected: failed, dest_tag not found
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_build(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual = stderr.getvalue()
         expected = self.format_error_message("Unknown destination tag: dest_tag_name")
         self.assertMultiLineEqual(actual, expected)
@@ -423,10 +412,6 @@ Task info: weburl/taskinfo?taskID=1
         self.session.build.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
     @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
@@ -455,8 +440,9 @@ Task info: weburl/taskinfo?taskID=1
         # Run it and check immediate output
         # args: target, http://scm
         # expected: failed, dest_tag is locked
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_build(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual = stderr.getvalue()
         expected = self.format_error_message("Destination tag dest_tag_name is locked")
         self.assertMultiLineEqual(actual, expected)
@@ -470,10 +456,6 @@ Task info: weburl/taskinfo?taskID=1
         self.session.build.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
@@ -838,7 +820,3 @@ Task info: weburl/taskinfo?taskID=1
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
         self.assertIsNone(rv)
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/tests/test_cli/test_call.py b/tests/test_cli/test_call.py
index 8ffcf89..dfba8b8 100644
--- a/tests/test_cli/test_call.py
+++ b/tests/test_cli/test_call.py
@@ -122,15 +122,12 @@ class TestCall(utils.CliTestCase):
 
         for mod, msg in module.items():
             with mock.patch('koji_cli.commands.%s' % mod, new=None):
-                with self.assertRaises(SystemExit) as cm:
+                with self.assertRaises(SystemExit) as ex:
                     handle_call(options, session, arguments)
+                self.assertExitCode(ex, 2)
             expected = self.format_error_message(msg)
             self.assert_console_message(stderr, expected)
             activate_session_mock.assert_not_called()
-            if isinstance(cm.exception, int):
-                self.assertEqual(cm.exception, 2)
-            else:
-                self.assertEqual(cm.exception.code, 2)
 
     def test_handle_call_help(self):
         """Test handle_call help message"""
diff --git a/tests/test_cli/test_chain_build.py b/tests/test_cli/test_chain_build.py
index b8ab16d..f98b7c9 100644
--- a/tests/test_cli/test_chain_build.py
+++ b/tests/test_cli/test_chain_build.py
@@ -10,8 +10,9 @@ except ImportError:
     import unittest
 
 from koji_cli.commands import handle_chain_build
+from . import utils
 
-class TestChainBuild(unittest.TestCase):
+class TestChainBuild(utils.CliTestCase):
     # Show long diffs in error output...
     maxDiff = None
 
@@ -100,8 +101,9 @@ Task info: weburl/taskinfo?taskID=1
         progname = os.path.basename(sys.argv[0]) or 'koji'
 
         # Run it and check immediate output
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_chain_build(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -122,10 +124,6 @@ Task info: weburl/taskinfo?taskID=1
         self.session.chainBuild.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
@@ -143,8 +141,9 @@ Task info: weburl/taskinfo?taskID=1
         progname = os.path.basename(sys.argv[0]) or 'koji'
 
         # Run it and check immediate output
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_chain_build(self.options, self.session, args)
+        self.assertExitCode(ex, 0)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = """Usage: %s chain-build [options] <target> <URL> [<URL> [:] <URL> [:] <URL> ...]
@@ -169,10 +168,6 @@ Options:
         self.session.chainBuild.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 0)
-        else:
-            self.assertEqual(cm.exception.code, 0)
 
     @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
@@ -203,8 +198,9 @@ Options:
         # Run it and check immediate output
         # args: target http://scm1 : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3
         # expected: failed, target not found
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_chain_build(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual = stderr.getvalue()
         expected = """Usage: %s chain-build [options] <target> <URL> [<URL> [:] <URL> [:] <URL> ...]
 (Specify the --help global option for a list of other help options)
@@ -221,10 +217,6 @@ Options:
         self.session.chainBuild.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
     @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
@@ -265,8 +257,9 @@ Options:
         # Run it and check immediate output
         # args: target http://scm1 : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3
         # expected: failed, dest_tag is locked
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_chain_build(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual = stderr.getvalue()
         expected = """Usage: %s chain-build [options] <target> <URL> [<URL> [:] <URL> [:] <URL> ...]
 (Specify the --help global option for a list of other help options)
@@ -283,17 +276,13 @@ Options:
         self.session.chainBuild.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)
     @mock.patch('koji_cli.commands.watch_tasks', return_value=0)
     def test_handle_build_dest_tag_not_inherited_by_build_tag(
-            self, watch_tasks_mock, running_in_bg_mock, activate_session_mock, stdout):
+            self, watch_tasks_mock, running_in_bg_mock, activate_session_mock, stderr):
         target = 'target'
         dest_tag = 'dest_tag'
         dest_tag_id = 2
@@ -324,8 +313,10 @@ Options:
         # Run it and check immediate output
         # args: target, target http://scm1 : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3
         # expected: failed, dest_tag is not in build_tag's inheritance
-        rv = handle_chain_build(self.options, self.session, args)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_chain_build(self.options, self.session, args)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = """Packages in destination tag dest_tag are not inherited by build tag build_tag
 Target target is not usable for a chain-build
 """
@@ -339,7 +330,6 @@ Target target is not usable for a chain-build
         self.session.chainBuild.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        self.assertEqual(rv, 1)
 
     @mock.patch('koji_cli.commands.activate_session')
     @mock.patch('koji_cli.commands._running_in_bg', return_value=False)
@@ -375,12 +365,14 @@ Target target is not usable for a chain-build
         self.session.getBuildTarget.return_value = target_info
         self.session.getTag.return_value = dest_tag_info
         self.session.getFullInheritance.return_value = tag_tree
-        with mock.patch('sys.stdout', new_callable=six.StringIO) as stdout:
+        with mock.patch('sys.stderr', new_callable=six.StringIO) as stderr:
             # Run it and check immediate output
             # args: target badnvr : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3
             # expected: failed, src is neither scm nor good n-v-r
-            rv = handle_chain_build(self.options, self.session, args)
-            actual = stdout.getvalue()
+            with self.assertRaises(SystemExit) as ex:
+                handle_chain_build(self.options, self.session, args)
+            self.assertExitCode(ex, 1)
+            actual = stderr.getvalue()
             expected = '"badnvr" is not a SCM URL or package N-V-R\n'
             self.assertMultiLineEqual(actual, expected)
             # Finally, assert that things were called as we expected.
@@ -394,9 +386,8 @@ Target target is not usable for a chain-build
             running_in_bg_mock.assert_not_called()
             self.session.logout.assert_not_called()
             watch_tasks_mock.assert_not_called()
-            self.assertEqual(rv, 1)
 
-        with mock.patch('sys.stdout', new_callable=six.StringIO) as stdout:
+        with mock.patch('sys.stderr', new_callable=six.StringIO) as stderr:
             source_args = [
                 'path/n-v-r',
                 ':',
@@ -409,12 +400,14 @@ Target target is not usable for a chain-build
             args = [target] + source_args
             # args: target path/n-v-r : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3
             # expected: failed
-            handle_chain_build(self.options, self.session, args)
-            actual = stdout.getvalue()
+            with self.assertRaises(SystemExit) as ex:
+                handle_chain_build(self.options, self.session, args)
+            self.assertExitCode(ex, 1)
+            actual = stderr.getvalue()
             expected = '"path/n-v-r" is not a SCM URL or package N-V-R\n'
             self.assertMultiLineEqual(actual, expected)
 
-        with mock.patch('sys.stdout', new_callable=six.StringIO) as stdout:
+        with mock.patch('sys.stderr', new_callable=six.StringIO) as stderr:
             source_args = [
                 'badn-vr',
                 ':',
@@ -427,12 +420,14 @@ Target target is not usable for a chain-build
             args = [target] + source_args
             # args: target badn-vr : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3
             # expected: failed
-            handle_chain_build(self.options, self.session, args)
-            actual = stdout.getvalue()
+            with self.assertRaises(SystemExit) as ex:
+                handle_chain_build(self.options, self.session, args)
+            self.assertExitCode(ex, 1)
+            actual = stderr.getvalue()
             expected = '"badn-vr" is not a SCM URL or package N-V-R\n'
             self.assertMultiLineEqual(actual, expected)
 
-        with mock.patch('sys.stdout', new_callable=six.StringIO) as stdout:
+        with mock.patch('sys.stderr', new_callable=six.StringIO) as stderr:
             source_args = [
                 'badn-v-r.rpm',
                 ':',
@@ -445,8 +440,10 @@ Target target is not usable for a chain-build
             args = [target] + source_args
             # args: target badn-v-r.rpm : http://scm2 http://scm3 n-v-r-1 : n-v-r-2 n-v-r-3
             # expected: failed
-            handle_chain_build(self.options, self.session, args)
-            actual = stdout.getvalue()
+            with self.assertRaises(SystemExit) as ex:
+                handle_chain_build(self.options, self.session, args)
+            self.assertExitCode(ex, 1)
+            actual = stderr.getvalue()
             expected = '"badn-v-r.rpm" is not a SCM URL or package N-V-R\n'
             self.assertMultiLineEqual(actual, expected)
 
@@ -458,8 +455,9 @@ Target target is not usable for a chain-build
 
             # args: target http://scm
             # expected: failed, only one src found
-            with self.assertRaises(SystemExit) as cm:
+            with self.assertRaises(SystemExit) as ex:
                 handle_chain_build(self.options, self.session, args)
+            self.assertExitCode(ex, 2)
             actual = stderr.getvalue()
             expected = """Usage: %s chain-build [options] <target> <URL> [<URL> [:] <URL> [:] <URL> ...]
 (Specify the --help global option for a list of other help options)
@@ -468,10 +466,6 @@ Target target is not usable for a chain-build
 If there are no dependencies, use the build command instead
 """ % (progname, progname)
             self.assertMultiLineEqual(actual, expected)
-            if isinstance(cm.exception, int):
-                self.assertEqual(cm.exception, 2)
-            else:
-                self.assertEqual(cm.exception.code, 2)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
diff --git a/tests/test_cli/test_disable_host.py b/tests/test_cli/test_disable_host.py
index 5f44aba..e8824e6 100644
--- a/tests/test_cli/test_disable_host.py
+++ b/tests/test_cli/test_disable_host.py
@@ -24,12 +24,14 @@ class TestDisableHost(utils.CliTestCase):
 """ % (self.progname, self.progname)
 
 
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
     def test_handle_disable_host(
             self,
             activate_session_mock,
-            stdout):
+            stdout,
+            stderr):
         """Test %s function""" % handle_disable_host.__name__
         arguments = []
         options = mock.MagicMock()
@@ -59,7 +61,9 @@ class TestDisableHost(utils.CliTestCase):
         session.multiCall.return_value = [[None], [None]]
 
         arguments = ['host1', 'host2']
-        self.assertEqual(1, handle_disable_host(options, session, arguments))
+        with self.assertRaises(SystemExit) as ex:
+            handle_disable_host(options, session, arguments)
+        self.assertExitCode(ex, 1)
         activate_session_mock.assert_called_once()
         session.getHost.assert_has_calls([call('host1'), call('host2')])
         session.multiCall.assert_called_once()
@@ -68,8 +72,8 @@ class TestDisableHost(utils.CliTestCase):
         expect = ''
         for host in arguments:
             expect += "Host %s does not exist\n" % host
-        expect += "No changes made. Please correct the command line.\n"
         self.assert_console_message(stdout, expect)
+        self.assert_console_message(stderr, "No changes made. Please correct the command line.\n")
 
         # reset session mocks
         activate_session_mock.reset_mock()
diff --git a/tests/test_cli/test_download_task.py b/tests/test_cli/test_download_task.py
index e89fd8f..b405e60 100644
--- a/tests/test_cli/test_download_task.py
+++ b/tests/test_cli/test_download_task.py
@@ -4,17 +4,14 @@ from mock import call
 import os
 import six
 import sys
-try:
-    import unittest2 as unittest
-except ImportError:
-    import unittest
 
 from koji_cli.commands import anon_handle_download_task
+from . import utils
 
 progname = os.path.basename(sys.argv[0]) or 'koji'
 
 
-class TestDownloadTask(unittest.TestCase):
+class TestDownloadTask(utils.CliTestCase):
     # Show long diffs in error output...
     maxDiff = None
 
@@ -98,8 +95,9 @@ class TestDownloadTask(unittest.TestCase):
         # Run it and check immediate output
         # args: task_id
         # expected: error
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             anon_handle_download_task(self.options, self.session, args)
+        self.assertExitCode(ex, 1)
         actual = self.stdout.getvalue()
         expected = ''
         self.assertMultiLineEqual(actual, expected)
@@ -111,10 +109,6 @@ class TestDownloadTask(unittest.TestCase):
                                                       self.options)
         self.session.getTaskInfo.assert_called_once_with(task_id)
         self.session.getTaskChildren.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 1)
-        else:
-            self.assertEqual(cm.exception.code, 1)
 
     def test_handle_download_task_parent(self):
         task_id = 123333
@@ -220,8 +214,9 @@ class TestDownloadTask(unittest.TestCase):
         # Run it and check immediate output
         # args: task_id --arch=s390,ppc
         # expected: failure
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             anon_handle_download_task(self.options, self.session, args)
+        self.assertExitCode(ex, 1)
         actual = self.stdout.getvalue()
         expected = ''
         self.assertMultiLineEqual(actual, expected)
@@ -234,10 +229,6 @@ class TestDownloadTask(unittest.TestCase):
         self.session.getTaskChildren.assert_not_called()
         self.list_task_output_all_volumes.assert_called_once_with(self.session, task_id)
         self.download_file.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 1)
-        else:
-            self.assertEqual(cm.exception.code, 1)
 
     def test_handle_download_parent_not_finished(self):
         task_id = 123333
@@ -256,8 +247,9 @@ class TestDownloadTask(unittest.TestCase):
         # Run it and check immediate output
         # args: task_id
         # expected: failure
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             anon_handle_download_task(self.options, self.session, args)
+        self.assertExitCode(ex, 1)
         actual = self.stdout.getvalue()
         expected = ''
         self.assertMultiLineEqual(actual, expected)
@@ -270,10 +262,6 @@ class TestDownloadTask(unittest.TestCase):
         self.session.getTaskChildren.assert_not_called()
         self.list_task_output_all_volumes.assert_called_once_with(self.session, task_id)
         self.download_file.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 1)
-        else:
-            self.assertEqual(cm.exception.code, 1)
 
     def test_handle_download_child_not_finished(self):
         task_id = 123333
@@ -290,8 +278,9 @@ class TestDownloadTask(unittest.TestCase):
         # Run it and check immediate output
         # args: task_id
         # expected: failure
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             anon_handle_download_task(self.options, self.session, args)
+        self.assertExitCode(ex, 1)
         actual = self.stdout.getvalue()
         expected = ''
         self.assertMultiLineEqual(actual, expected)
@@ -304,10 +293,6 @@ class TestDownloadTask(unittest.TestCase):
         self.session.getTaskChildren.assert_called_once_with(task_id)
         self.list_task_output_all_volumes.assert_called_once_with(self.session, 22222)
         self.download_file.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 1)
-        else:
-            self.assertEqual(cm.exception.code, 1)
 
     def test_handle_download_invalid_file_name(self):
         task_id = 123333
@@ -320,8 +305,9 @@ class TestDownloadTask(unittest.TestCase):
         # Run it and check immediate output
         # args: task_id
         # expected: failure
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             anon_handle_download_task(self.options, self.session, args)
+        self.assertExitCode(ex, 1)
         actual = self.stdout.getvalue()
         expected = ''
         self.assertMultiLineEqual(actual, expected)
@@ -334,18 +320,15 @@ class TestDownloadTask(unittest.TestCase):
         self.session.getTaskChildren.assert_not_called()
         self.list_task_output_all_volumes.assert_called_once_with(self.session, task_id)
         self.download_file.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 1)
-        else:
-            self.assertEqual(cm.exception.code, 1)
 
     def test_handle_download_help(self):
         args = ['--help']
         # Run it and check immediate output
         # args: --help
         # expected: failure
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             anon_handle_download_task(self.options, self.session, args)
+        self.assertExitCode(ex, 0)
         actual = self.stdout.getvalue()
         expected = """Usage: %s download-task <task_id>
 (Specify the --help global option for a list of other help options)
@@ -363,18 +346,15 @@ Options:
         actual = self.stderr.getvalue()
         expected = ''
         self.assertEqual(actual, expected)
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 0)
-        else:
-            self.assertEqual(cm.exception.code, 0)
 
     def test_handle_download_no_task_id(self):
         args = []
         # Run it and check immediate output
         # no args
         # expected: failure
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             anon_handle_download_task(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual = self.stdout.getvalue()
         expected = ''
         self.assertMultiLineEqual(actual, expected)
@@ -385,18 +365,15 @@ Options:
 %s: error: Please specify a task ID
 """ % (progname, progname)
         self.assertEqual(actual, expected)
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
     def test_handle_download_multi_task_id(self):
         args = ["123", "456"]
         # Run it and check immediate output
         # args: 123 456
         # expected: failure
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             anon_handle_download_task(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual = self.stdout.getvalue()
         expected = ''
         self.assertMultiLineEqual(actual, expected)
@@ -407,11 +384,3 @@ Options:
 %s: error: Only one task ID may be specified
 """ % (progname, progname)
         self.assertEqual(actual, expected)
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/tests/test_cli/test_edit_host.py b/tests/test_cli/test_edit_host.py
index 6b5641f..578db7a 100644
--- a/tests/test_cli/test_edit_host.py
+++ b/tests/test_cli/test_edit_host.py
@@ -11,8 +11,9 @@ except ImportError:
 from mock import call
 
 from koji_cli.commands import handle_edit_host
+from . import utils
 
-class TestEditHost(unittest.TestCase):
+class TestEditHost(utils.CliTestCase):
 
     # Show long diffs in error output...
     maxDiff = None
@@ -157,8 +158,9 @@ class TestEditHost(unittest.TestCase):
         # Run it and check immediate output
         # args: _empty_
         # expected: failed - should specify host
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_edit_host(options, session, args)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -175,14 +177,10 @@ class TestEditHost(unittest.TestCase):
         session.getHost.assert_not_called()
         session.editHost.assert_not_called()
         session.multiCall.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
-    def test_handle_edit_host_no_host(self, activate_session_mock, stdout):
+    def test_handle_edit_host_no_host(self, activate_session_mock, stderr):
         host = 'host'
         host_info = None
         arches = 'arch1 arch2'
@@ -204,8 +202,10 @@ class TestEditHost(unittest.TestCase):
         # args: host, --arches='arch1 arch2', --capacity=0.22,
         # --description=description, --comment=comment
         # expected: failed -- getHost() == None
-        rv = handle_edit_host(options, session, args)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_edit_host(options, session, args)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = """Host host does not exist
 No changes made, please correct the command line
 """
@@ -215,7 +215,6 @@ No changes made, please correct the command line
         session.getHost.assert_called_once_with(host)
         session.editHost.assert_not_called()
         self.assertEqual(session.multiCall.call_count, 1)
-        self.assertEqual(rv, 1)
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_cli/test_edit_tag.py b/tests/test_cli/test_edit_tag.py
index b08404b..719dcbb 100644
--- a/tests/test_cli/test_edit_tag.py
+++ b/tests/test_cli/test_edit_tag.py
@@ -3,17 +3,14 @@ import mock
 import os
 import six
 import sys
-try:
-    import unittest2 as unittest
-except ImportError:
-    import unittest
 
 from koji_cli.commands import handle_edit_tag
+from . import utils
 
 progname = os.path.basename(sys.argv[0]) or 'koji'
 
 
-class TestEditTag(unittest.TestCase):
+class TestEditTag(utils.CliTestCase):
     # Show long diffs in error output...
     maxDiff = None
 
@@ -107,8 +104,9 @@ class TestEditTag(unittest.TestCase):
         # Run it and check immediate output
         # args: --help
         # expected: failed, help info shows
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_edit_tag(options, session, args)
+        self.assertExitCode(ex, 0)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = """Usage: %s edit-tag [options] <name>
@@ -139,10 +137,6 @@ Options:
         # Finally, assert that things were called as we expected.
         activate_session_mock.assert_not_called()
         session.editTag2.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 0)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
@@ -157,8 +151,9 @@ Options:
         # Run it and check immediate output
         # args: --help
         # expected: failed, help info shows
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_edit_tag(options, session, args)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -172,11 +167,3 @@ Options:
         # Finally, assert that things were called as we expected.
         activate_session_mock.assert_not_called()
         session.editTag2.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/tests/test_cli/test_edit_user.py b/tests/test_cli/test_edit_user.py
index 12e43af..5c79920 100644
--- a/tests/test_cli/test_edit_user.py
+++ b/tests/test_cli/test_edit_user.py
@@ -10,11 +10,12 @@ except ImportError:
 
 
 from koji_cli.commands import handle_edit_user
+from . import utils
 
 progname = os.path.basename(sys.argv[0]) or 'koji'
 
 
-class TestEditUser(unittest.TestCase):
+class TestEditUser(utils.CliTestCase):
     # Show long diffs in error output...
     maxDiff = None
 
@@ -101,8 +102,9 @@ Options:
         # Run it and check immediate output
         # args: --help
         # expected: failed, help info shows
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_edit_user(options, session, args)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -116,11 +118,6 @@ Options:
         # Finally, assert that things were called as we expected.
         activate_session_mock.assert_not_called()
         session.editUser.assert_not_called()
-        if isinstance(cm.exception, int):
-            # RHEL6
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
 
 if __name__ == '__main__':
diff --git a/tests/test_cli/test_enable_host.py b/tests/test_cli/test_enable_host.py
index 20fc5ee..fc5941f 100644
--- a/tests/test_cli/test_enable_host.py
+++ b/tests/test_cli/test_enable_host.py
@@ -23,12 +23,14 @@ class TestEnableHost(utils.CliTestCase):
 %s: error: {message}
 """ % (self.progname, self.progname)
 
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
     def test_handle_enable_host(
             self,
             activate_session_mock,
-            stdout):
+            stdout,
+            stderr):
         """Test %s function""" % handle_enable_host.__name__
         arguments = []
         options = mock.MagicMock()
@@ -58,7 +60,9 @@ class TestEnableHost(utils.CliTestCase):
         session.multiCall.return_value = [[None], [None]]
 
         arguments = ['host1', 'host2']
-        self.assertEqual(1, handle_enable_host(options, session, arguments))
+        with self.assertRaises(SystemExit) as ex:
+            handle_enable_host(options, session, arguments)
+        self.assertExitCode(ex, 1)
         activate_session_mock.assert_called_once()
         session.getHost.assert_has_calls([call('host1'), call('host2')])
         session.multiCall.assert_called_once()
@@ -67,8 +71,9 @@ class TestEnableHost(utils.CliTestCase):
         expect = ''
         for host in arguments:
             expect += "Host %s does not exist\n" % host
-        expect += "No changes made. Please correct the command line.\n"
+        stderr_exp = "No changes made. Please correct the command line.\n"
         self.assert_console_message(stdout, expect)
+        self.assert_console_message(stderr, stderr_exp)
 
         # reset session mocks
         activate_session_mock.reset_mock()
diff --git a/tests/test_cli/test_import_comps.py b/tests/test_cli/test_import_comps.py
index 1f78b87..18abae4 100644
--- a/tests/test_cli/test_import_comps.py
+++ b/tests/test_cli/test_import_comps.py
@@ -23,8 +23,9 @@ from nose.plugins.skip import SkipTest
 import koji_cli.commands
 from koji_cli.commands import handle_import_comps, _import_comps,\
                               _import_comps_alt
+from . import utils
 
-class TestImportComps(unittest.TestCase):
+class TestImportComps(utils.CliTestCase):
     # Show long diffs in error output...
     maxDiff = None
 
@@ -109,7 +110,7 @@ class TestImportComps(unittest.TestCase):
             session, filename, tag, local_options)
         self.assertNotEqual(rv, 1)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.libcomps', new=None)
     @mock.patch('koji_cli.commands.yumcomps', new=None, create=True)
     @mock.patch('koji_cli.commands.activate_session')
@@ -120,7 +121,7 @@ class TestImportComps(unittest.TestCase):
             mock_import_comps_alt,
             mock_import_comps,
             mock_activate_session,
-            stdout):
+            stderr):
         filename = './data/comps-example.xml'
         tag = 'tag'
         tag_info = {'name': tag, 'id': 1}
@@ -134,8 +135,10 @@ class TestImportComps(unittest.TestCase):
         # Run it and check immediate output
         # args: --force, ./data/comps-example.xml, tag
         # expected: failed, no comps available
-        rv = handle_import_comps(options, session, args)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_import_comps(options, session, args)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = 'comps module not available\n'
         self.assertMultiLineEqual(actual, expected)
 
@@ -144,9 +147,8 @@ class TestImportComps(unittest.TestCase):
         session.getTag.assert_called_once_with(tag)
         mock_import_comps.assert_not_called()
         mock_import_comps_alt.assert_not_called()
-        self.assertEqual(rv, 1)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
     @mock.patch('koji_cli.commands._import_comps')
     @mock.patch('koji_cli.commands._import_comps_alt')
@@ -155,7 +157,7 @@ class TestImportComps(unittest.TestCase):
             mock_import_comps_alt,
             mock_import_comps,
             mock_activate_session,
-            stdout):
+            stderr):
         filename = './data/comps-example.xml'
         tag = 'tag'
         tag_info = None
@@ -169,8 +171,10 @@ class TestImportComps(unittest.TestCase):
         # Run it and check immediate output
         # args: ./data/comps-example.xml, tag
         # expected: failed: tag does not exist
-        rv = handle_import_comps(options, session, args)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_import_comps(options, session, args)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = 'No such tag: tag\n'
         self.assertMultiLineEqual(actual, expected)
 
@@ -179,7 +183,6 @@ class TestImportComps(unittest.TestCase):
         session.getTag.assert_called_once_with(tag)
         mock_import_comps.assert_not_called()
         mock_import_comps_alt.assert_not_called()
-        self.assertEqual(rv, 1)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
@@ -200,8 +203,9 @@ class TestImportComps(unittest.TestCase):
         session = mock.MagicMock()
 
         # Run it and check immediate output
-        with self.assertRaises(SystemExit) as cm:
-            rv = handle_import_comps(options, session, args)
+        with self.assertRaises(SystemExit) as ex:
+            handle_import_comps(options, session, args)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -218,10 +222,6 @@ class TestImportComps(unittest.TestCase):
         session.getTag.assert_not_called()
         session.getTagGroups.assert_not_called()
         session.groupListAdd.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     def test_import_comps_libcomps(self, stdout):
diff --git a/tests/test_cli/test_list_permissions.py b/tests/test_cli/test_list_permissions.py
index b817d1f..1afb215 100644
--- a/tests/test_cli/test_list_permissions.py
+++ b/tests/test_cli/test_list_permissions.py
@@ -22,12 +22,14 @@ class TestListPermissions(utils.CliTestCase):
 %s: error: {message}
 """ % (self.progname, self.progname)
 
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
     def test_handle_list_permissions(
             self,
             activate_session_mock,
-            stdout):
+            stdout,
+            stderr):
         """Test handle_list_permissions function"""
         session = mock.MagicMock()
         options = mock.MagicMock()
@@ -54,8 +56,10 @@ class TestListPermissions(utils.CliTestCase):
         # case 2. user does not exists
         expected = "User %s does not exist" % user + "\n"
         session.getUser.return_value = None
-        self.assertEqual(1, handle_list_permissions(options, session, ['--user', user]))
-        self.assert_console_message(stdout, expected)
+        with self.assertRaises(SystemExit) as ex:
+            handle_list_permissions(options, session, ['--user', user])
+        self.assertExitCode(ex, 1)
+        self.assert_console_message(stderr, expected)
 
         # case 3. List user permission
         perms = [p['name'] for p in all_perms[::1]]
diff --git a/tests/test_cli/test_maven_build.py b/tests/test_cli/test_maven_build.py
index fac3cb5..f21086c 100644
--- a/tests/test_cli/test_maven_build.py
+++ b/tests/test_cli/test_maven_build.py
@@ -11,6 +11,7 @@ except ImportError:
     import unittest
 
 from koji_cli.commands import handle_maven_build
+from . import utils
 
 EMPTY_BUILD_OPTS = {
     'specfile': None,
@@ -32,7 +33,7 @@ EMPTY_BUILD_OPTS = {
     'inis': []}
 
 
-class TestMavenBuild(unittest.TestCase):
+class TestMavenBuild(utils.CliTestCase):
     # Show long diffs in error output...
     maxDiff = None
 
@@ -103,8 +104,9 @@ Task info: weburl/taskinfo?taskID=1
         progname = os.path.basename(sys.argv[0]) or 'koji'
 
         # Run it and check immediate output
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_maven_build(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -125,10 +127,6 @@ Task info: weburl/taskinfo?taskID=1
         self.session.mavenBuild.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
@@ -146,8 +144,9 @@ Task info: weburl/taskinfo?taskID=1
         progname = os.path.basename(sys.argv[0]) or 'koji'
 
         # Run it and check immediate output
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_maven_build(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -168,10 +167,6 @@ Task info: weburl/taskinfo?taskID=1
         self.session.mavenBuild.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
@@ -189,8 +184,9 @@ Task info: weburl/taskinfo?taskID=1
         progname = os.path.basename(sys.argv[0]) or 'koji'
 
         # Run it and check immediate output
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_maven_build(self.options, self.session, args)
+        self.assertExitCode(ex, 0)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = """Usage: %s maven-build [options] <target> <URL>
@@ -239,10 +235,6 @@ Options:
         self.session.mavenBuild.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 0)
 
     @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
@@ -265,8 +257,9 @@ Options:
         # Run it and check immediate output
         # args: target http://scm
         # expected: failed, target not found
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_maven_build(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual = stderr.getvalue()
         expected = """Usage: %s maven-build [options] <target> <URL>
        %s maven-build --ini=CONFIG... [options] <target>
@@ -283,10 +276,6 @@ Options:
         self.session.mavenBuild.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
     @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
@@ -313,8 +302,9 @@ Options:
         # Run it and check immediate output
         # args: target http://scm
         # expected: failed, dest_tag not found
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_maven_build(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual = stderr.getvalue()
         expected = """Usage: %s maven-build [options] <target> <URL>
        %s maven-build --ini=CONFIG... [options] <target>
@@ -331,10 +321,6 @@ Options:
         self.session.mavenBuild.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
     @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
@@ -361,8 +347,9 @@ Options:
         # Run it and check immediate output
         # args: target http://scm
         # expected: failed, dest_tag is locked
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_maven_build(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual = stderr.getvalue()
         expected = """Usage: %s maven-build [options] <target> <URL>
        %s maven-build --ini=CONFIG... [options] <target>
@@ -379,10 +366,6 @@ Options:
         self.session.mavenBuild.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
     @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('sys.stdout', new_callable=six.StringIO)
@@ -473,8 +456,9 @@ Task info: weburl/taskinfo?taskID=1
         # Run it and check immediate output
         # args: --ini=config1.ini --ini=config2.ini --section=section target
         # expected: failed, no type == 'maven' found
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_maven_build(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual = stderr.getvalue()
         expected = """Usage: %s maven-build [options] <target> <URL>
        %s maven-build --ini=CONFIG... [options] <target>
@@ -488,10 +472,6 @@ Task info: weburl/taskinfo?taskID=1
             build_opts.inis, scratch=scratch, section=section)
         maven_opts_mock.assert_not_called()
         self.session.mavenBuild.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
         stdout.seek(0)
         stdout.truncate()
@@ -504,8 +484,9 @@ Task info: weburl/taskinfo?taskID=1
         # Run it and check immediate output
         # args: --ini=config1.ini --ini=config2.ini --section=section target
         # expected: failed, ValueError raised when parsing .ini files
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_maven_build(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual = stderr.getvalue()
         expected = """Usage: %s maven-build [options] <target> <URL>
        %s maven-build --ini=CONFIG... [options] <target>
@@ -519,10 +500,6 @@ Task info: weburl/taskinfo?taskID=1
             build_opts.inis, scratch=scratch, section=section)
         maven_opts_mock.assert_not_called()
         self.session.mavenBuild.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
     @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
@@ -554,8 +531,9 @@ Task info: weburl/taskinfo?taskID=1
         # Run it and check immediate output
         # args: target badscm
         # expected: failed, scm is invalid
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_maven_build(self.options, self.session, args)
+        self.assertExitCode(ex, 2)
         actual = stderr.getvalue()
         expected = """Usage: %s maven-build [options] <target> <URL>
        %s maven-build --ini=CONFIG... [options] <target>
@@ -574,10 +552,6 @@ Task info: weburl/taskinfo?taskID=1
         self.session.mavenBuild.assert_not_called()
         self.session.logout.assert_not_called()
         watch_tasks_mock.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
diff --git a/tests/test_cli/test_mock_config.py b/tests/test_cli/test_mock_config.py
index 1ca0d0b..028c806 100644
--- a/tests/test_cli/test_mock_config.py
+++ b/tests/test_cli/test_mock_config.py
@@ -142,8 +142,10 @@ config_opts['macros']['%distribution'] = 'Koji Testing'
 
         arguments = ['--task', str(task_id)]
         expected = "No buildroots for task %s (or no such task)\n" % str(task_id)
-        self.assertEqual(1, anon_handle_mock_config(options, session, arguments))
-        self.assert_console_message(stdout, expected)
+        with self.assertRaises(SystemExit) as ex:
+            anon_handle_mock_config(options, session, arguments)
+        self.assertExitCode(ex, 1)
+        self.assert_console_message(stderr, expected)
 
         multi_broots = [
             {'id': 1101, 'repo_id': 101, 'tag_name': 'tag_101', 'arch': 'x86_64'},
@@ -191,8 +193,10 @@ config_opts['macros']['%distribution'] = 'Koji Testing'
 
         arguments = ['--tag', tag['name']]
         expected = "Please specify an arch\n"
-        self.assertEqual(1, anon_handle_mock_config(options, session, arguments))
-        self.assert_console_message(stdout, expected)
+        with self.assertRaises(SystemExit) as ex:
+            anon_handle_mock_config(options, session, arguments)
+        self.assertExitCode(ex, 1)
+        self.assert_console_message(stderr, expected)
 
         arguments = ['--tag', tag['name'], '--arch', tag['arch']]
         expected = self.format_error_message("Invalid tag: %s" % tag['name'])
@@ -206,14 +210,18 @@ config_opts['macros']['%distribution'] = 'Koji Testing'
         # return tag info
         session.getTag.return_value = tag
         expected = "Could not get config info for tag: %(name)s\n" % tag
-        self.assertEqual(1, anon_handle_mock_config(options, session, arguments))
-        self.assert_console_message(stdout, expected)
+        with self.assertRaises(SystemExit) as ex:
+            anon_handle_mock_config(options, session, arguments)
+        self.assertExitCode(ex, 1)
+        self.assert_console_message(stderr, expected)
 
         # return build config
         session.getBuildConfig.return_value = {'id': 301, 'extra': {}}
         expected = "Could not get a repo for tag: %(name)s\n" % tag
-        self.assertEqual(1, anon_handle_mock_config(options, session, arguments))
-        self.assert_console_message(stdout, expected)
+        with self.assertRaises(SystemExit) as ex:
+            anon_handle_mock_config(options, session, arguments)
+        self.assertExitCode(ex, 1)
+        self.assert_console_message(stderr, expected)
 
         # return repo
         session.getRepo.return_value = {'id': 101}
@@ -262,8 +270,10 @@ config_opts['macros']['%distribution'] = 'Koji Testing'
 
         arguments = ['--target', target['name']]
         expected = "Please specify an arch\n"
-        self.assertEqual(1, anon_handle_mock_config(options, session, arguments))
-        self.assert_console_message(stdout, expected)
+        with self.assertRaises(SystemExit) as ex:
+            anon_handle_mock_config(options, session, arguments)
+        self.assertExitCode(ex, 1)
+        self.assert_console_message(stderr, expected)
 
         arguments = ['--target', target['name'],
                      '--arch', arch]
@@ -278,8 +288,10 @@ config_opts['macros']['%distribution'] = 'Koji Testing'
 
         session.getBuildTarget.return_value = target
         expected = "Could not get a repo for tag: %s\n" % target['build_tag_name']
-        self.assertEqual(1, anon_handle_mock_config(options, session, arguments))
-        self.assert_console_message(stdout, expected)
+        with self.assertRaises(SystemExit) as ex:
+            anon_handle_mock_config(options, session, arguments)
+        self.assertExitCode(ex, 1)
+        self.assert_console_message(stderr, expected)
 
         arguments = self.common_args + ['--target', target['name'],
                                         '--arch', arch,
diff --git a/tests/test_cli/test_remove_channel.py b/tests/test_cli/test_remove_channel.py
index 61fe9d5..a82ca69 100644
--- a/tests/test_cli/test_remove_channel.py
+++ b/tests/test_cli/test_remove_channel.py
@@ -9,9 +9,10 @@ except ImportError:
     import unittest
 
 from koji_cli.commands import handle_remove_channel
+from . import utils
 
 
-class TestRemoveChannel(unittest.TestCase):
+class TestRemoveChannel(utils.CliTestCase):
 
     # Show long diffs in error output...
     maxDiff = None
@@ -67,10 +68,10 @@ class TestRemoveChannel(unittest.TestCase):
         session.removeChannel.assert_called_once_with(channel, force=True)
         self.assertNotEqual(rv, 1)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
     def test_handle_remove_channel_no_channel(
-            self, activate_session_mock, stdout):
+            self, activate_session_mock, stderr):
         channel = 'channel'
         channel_info = None
         args = [channel]
@@ -83,15 +84,16 @@ class TestRemoveChannel(unittest.TestCase):
         # Run it and check immediate output
         # args: channel
         # expected: failed: no such channel
-        rv = handle_remove_channel(options, session, args)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_remove_channel(options, session, args)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = 'No such channel: channel\n'
         self.assertMultiLineEqual(actual, expected)
         # Finally, assert that things were called as we expected.
         activate_session_mock.assert_called_once_with(session, options)
         session.getChannel.assert_called_once_with(channel)
         session.removeChannel.assert_not_called()
-        self.assertEqual(rv, 1)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
@@ -106,8 +108,9 @@ class TestRemoveChannel(unittest.TestCase):
         session = mock.MagicMock()
 
         # Run it and check immediate output
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_remove_channel(options, session, args)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -123,10 +126,6 @@ class TestRemoveChannel(unittest.TestCase):
         activate_session_mock.assert_not_called()
         session.getChannel.assert_not_called()
         session.removeChannel.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
 
 if __name__ == '__main__':
diff --git a/tests/test_cli/test_remove_host_from_channel.py b/tests/test_cli/test_remove_host_from_channel.py
index 35c9cba..a0dd8a1 100644
--- a/tests/test_cli/test_remove_host_from_channel.py
+++ b/tests/test_cli/test_remove_host_from_channel.py
@@ -9,8 +9,9 @@ except ImportError:
     import unittest
 
 from koji_cli.commands import handle_remove_host_from_channel
+from . import utils
 
-class TestRemoveHostFromChannel(unittest.TestCase):
+class TestRemoveHostFromChannel(utils.CliTestCase):
 
     # Show long diffs in error output...
     maxDiff = None
@@ -45,10 +46,10 @@ class TestRemoveHostFromChannel(unittest.TestCase):
         session.removeHostFromChannel.assert_called_once_with(host, channel)
         self.assertNotEqual(rv, 1)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
     def test_handle_remove_host_from_channel_no_host(
-            self, activate_session_mock, stdout):
+            self, activate_session_mock, stderr):
         host = 'host'
         host_info = None
         channel = 'channel'
@@ -62,8 +63,10 @@ class TestRemoveHostFromChannel(unittest.TestCase):
         # Run it and check immediate output
         # args: host, channel
         # expected: failed: no such host
-        rv = handle_remove_host_from_channel(options, session, args)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_remove_host_from_channel(options, session, args)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = 'No such host: host\n'
         self.assertMultiLineEqual(actual, expected)
         # Finally, assert that things were called as we expected.
@@ -71,12 +74,11 @@ class TestRemoveHostFromChannel(unittest.TestCase):
         session.getHost.assert_called_once_with(host)
         session.listChannels.assert_not_called()
         session.removeHostFromChannel.assert_not_called()
-        self.assertEqual(rv, 1)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
     def test_handle_remove_host_from_channel_not_a_member(
-            self, activate_session_mock, stdout):
+            self, activate_session_mock, stderr):
         host = 'host'
         host_info = {'id': 1}
         channel = 'channel'
@@ -92,9 +94,11 @@ class TestRemoveHostFromChannel(unittest.TestCase):
         session.listChannels.return_value = channel_infos
         # Run it and check immediate output
         # args: host, channel
-        # expected: success: host isn't belong to channel
-        rv = handle_remove_host_from_channel(options, session, args)
-        actual = stdout.getvalue()
+        # expected: failed: host isn't belong to channel
+        with self.assertRaises(SystemExit) as ex:
+            handle_remove_host_from_channel(options, session, args)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = 'Host host is not a member of channel channel\n'
         self.assertMultiLineEqual(actual, expected)
         # Finally, assert that things were called as we expected.
@@ -102,7 +106,6 @@ class TestRemoveHostFromChannel(unittest.TestCase):
         session.getHost.assert_called_once_with(host)
         session.listChannels.assert_called_once_with(host_info['id'])
         session.removeHostFromChannel.assert_not_called()
-        self.assertEqual(rv, 1)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
@@ -119,8 +122,9 @@ class TestRemoveHostFromChannel(unittest.TestCase):
         # Run it and check immediate output
         # args: _empty_
         # expected: failed, help msg shows
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_remove_host_from_channel(options, session, args)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -137,10 +141,6 @@ class TestRemoveHostFromChannel(unittest.TestCase):
         session.getHost.assert_not_called()
         session.listChannels.assert_not_called()
         session.removeHostFromChannel.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
 
 if __name__ == '__main__':
diff --git a/tests/test_cli/test_remove_pkg.py b/tests/test_cli/test_remove_pkg.py
index bf8e366..8c5df10 100644
--- a/tests/test_cli/test_remove_pkg.py
+++ b/tests/test_cli/test_remove_pkg.py
@@ -10,17 +10,17 @@ except ImportError:
 
 from mock import call
 
-
 from koji_cli.commands import handle_remove_pkg
+from . import utils
 
-class TestRemovePkg(unittest.TestCase):
+class TestRemovePkg(utils.CliTestCase):
 
     # Show long diffs in error output...
     maxDiff = None
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
-    def test_handle_remove_pkg(self, activate_session_mock, stdout):
+    def test_handle_remove_pkg(self, activate_session_mock, stderr):
         tag = 'tag'
         dsttag = {'name': tag, 'id': 1}
         package = 'package'
@@ -37,8 +37,8 @@ class TestRemovePkg(unittest.TestCase):
         # Run it and check immediate output
         # args: tag, package
         # expected: success
-        rv = handle_remove_pkg(options, session, args)
-        actual = stdout.getvalue()
+        handle_remove_pkg(options, session, args)
+        actual = stderr.getvalue()
         expected = ''
         self.assertMultiLineEqual(actual, expected)
         # Finally, assert that things were called as we expected.
@@ -49,7 +49,6 @@ class TestRemovePkg(unittest.TestCase):
         session.packageListRemove.assert_called_once_with(
             tag, package, **kwargs)
         session.multiCall.assert_called_once_with(strict=True)
-        self.assertNotEqual(rv, 1)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
@@ -129,9 +128,9 @@ class TestRemovePkg(unittest.TestCase):
                             strict=True)])
         self.assertNotEqual(rv, 1)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
-    def test_handle_remove_pkg_no_package(self, activate_session_mock, stdout):
+    def test_handle_remove_pkg_no_package(self, activate_session_mock, stderr):
         tag = 'tag'
         dsttag = {'name': tag, 'id': 1}
         packages = ['package1', 'package2', 'package3']
@@ -149,8 +148,10 @@ class TestRemovePkg(unittest.TestCase):
         # Run it and check immediate output
         # args: tag, package1, package2, package3
         # expected: failed: can not find package2 under tag
-        rv = handle_remove_pkg(options, session, args)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_remove_pkg(options, session, args)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = 'Package package2 is not in tag tag\n'
         self.assertMultiLineEqual(actual, expected)
         # Finally, assert that things were called as we expected.
@@ -160,12 +161,11 @@ class TestRemovePkg(unittest.TestCase):
             tagID=dsttag['id'])
         session.packageListRemove.assert_not_called()
         session.multiCall.assert_not_called()
-        self.assertEqual(rv, 1)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
     def test_handle_remove_pkg_tag_no_exists(
-            self, activate_session_mock, stdout):
+            self, activate_session_mock, stderr):
         tag = 'tag'
         dsttag = None
         packages = ['package1', 'package2', 'package3']
@@ -179,8 +179,10 @@ class TestRemovePkg(unittest.TestCase):
         # Run it and check immediate output
         # args: tag, package1, package2, package3
         # expected: failed: tag does not exist
-        rv = handle_remove_pkg(options, session, args)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_remove_pkg(options, session, args)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = 'No such tag: tag\n'
         self.assertMultiLineEqual(actual, expected)
         # Finally, assert that things were called as we expected.
@@ -188,7 +190,6 @@ class TestRemovePkg(unittest.TestCase):
         session.getTag.assert_called_once_with(tag)
         session.listPackages.assert_not_called()
         session.packageListRemove.assert_not_called()
-        self.assertEqual(rv, 1)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
@@ -204,8 +205,9 @@ class TestRemovePkg(unittest.TestCase):
         session = mock.MagicMock()
 
         # Run it and check immediate output
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_remove_pkg(options, session, args)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -222,10 +224,6 @@ class TestRemovePkg(unittest.TestCase):
         session.getTag.assert_not_called()
         session.listPackages.assert_not_called()
         session.packageListRemove.assert_not_called()
-        if isinstance(cm.exception, int):
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
 
 if __name__ == '__main__':
diff --git a/tests/test_cli/test_rename_channel.py b/tests/test_cli/test_rename_channel.py
index 9d0835b..45857e6 100644
--- a/tests/test_cli/test_rename_channel.py
+++ b/tests/test_cli/test_rename_channel.py
@@ -9,9 +9,10 @@ except ImportError:
     import unittest
 
 from koji_cli.commands import handle_rename_channel
+from . import utils
 
 
-class TestRenameChannel(unittest.TestCase):
+class TestRenameChannel(utils.CliTestCase):
 
     # Show long diffs in error output...
     maxDiff = None
@@ -42,10 +43,10 @@ class TestRenameChannel(unittest.TestCase):
         session.renameChannel.assert_called_once_with(old_name, new_name)
         self.assertNotEqual(rv, 1)
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.activate_session')
     def test_handle_rename_channel_no_channel(
-            self, activate_session_mock, stdout):
+            self, activate_session_mock, stderr):
         old_name = 'old_name'
         new_name = 'new_name'
         channel_info = None
@@ -59,15 +60,16 @@ class TestRenameChannel(unittest.TestCase):
         # Run it and check immediate output
         # args: old_name, new_name
         # expected: failed: no such channel
-        rv = handle_rename_channel(options, session, args)
-        actual = stdout.getvalue()
+        with self.assertRaises(SystemExit) as ex:
+            handle_rename_channel(options, session, args)
+        self.assertExitCode(ex, 1)
+        actual = stderr.getvalue()
         expected = 'No such channel: old_name\n'
         self.assertMultiLineEqual(actual, expected)
         # Finally, assert that things were called as we expected.
         activate_session_mock.assert_called_once_with(session, options)
         session.getChannel.assert_called_once_with(old_name)
         session.renameChannel.assert_not_called()
-        self.assertEqual(rv, 1)
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('sys.stderr', new_callable=six.StringIO)
@@ -82,8 +84,9 @@ class TestRenameChannel(unittest.TestCase):
         session = mock.MagicMock()
 
         # Run it and check immediate output
-        with self.assertRaises(SystemExit) as cm:
+        with self.assertRaises(SystemExit) as ex:
             handle_rename_channel(options, session, args)
+        self.assertExitCode(ex, 2)
         actual_stdout = stdout.getvalue()
         actual_stderr = stderr.getvalue()
         expected_stdout = ''
@@ -99,11 +102,6 @@ class TestRenameChannel(unittest.TestCase):
         activate_session_mock.assert_not_called()
         session.getChannel.assert_not_called()
         session.renameChannel.assert_not_called()
-        if isinstance(cm.exception, int):
-            # python 2.6.6
-            self.assertEqual(cm.exception, 2)
-        else:
-            self.assertEqual(cm.exception.code, 2)
 
 
 if __name__ == '__main__':
diff --git a/tests/test_cli/test_restart_host.py b/tests/test_cli/test_restart_host.py
index a70c919..296c2ae 100644
--- a/tests/test_cli/test_restart_host.py
+++ b/tests/test_cli/test_restart_host.py
@@ -48,6 +48,7 @@ class TestRestartHosts(utils.CliTestCase):
         watch_tasks_mock.assert_called_with(
             session, [self.task_id], quiet=None, poll_interval=3)
 
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     @mock.patch('koji_cli.commands.watch_tasks')
     @mock.patch('koji_cli.commands._running_in_bg')
@@ -57,7 +58,8 @@ class TestRestartHosts(utils.CliTestCase):
             activate_session_mock,
             running_in_bg_mock,
             watch_tasks_mock,
-            stdout):
+            stdout,
+            stderr):
         """Test %s function when there has other restart tasks exist""" % handle_restart_hosts.__name__
         options = mock.MagicMock()
         session = mock.MagicMock()
@@ -99,7 +101,9 @@ class TestRestartHosts(utils.CliTestCase):
         # has other restart tasks are running case
         session.listTasks.return_value = [{'id': 1}, {'id': 2}, {'id': 3}]
 
-        self.assertEqual(1, handle_restart_hosts(options, session, []))
+        with self.assertRaises(SystemExit) as ex:
+            handle_restart_hosts(options, session, [])
+        self.assertExitCode(ex, 1)
         activate_session_mock.assert_called_once()
 
         query_opt = {
@@ -115,7 +119,7 @@ class TestRestartHosts(utils.CliTestCase):
         expect += "Task ids: %r\n" % \
             [t['id'] for t in session.listTasks.return_value]
         expect += "Use --force to run anyway\n"
-        self.assert_console_message(stdout, expect)
+        self.assert_console_message(stderr, expect)
 
     @mock.patch('koji_cli.commands.watch_tasks')
     @mock.patch('koji_cli.commands._running_in_bg')
diff --git a/tests/test_cli/test_set_build_volume.py b/tests/test_cli/test_set_build_volume.py
index 85c1850..357527e 100644
--- a/tests/test_cli/test_set_build_volume.py
+++ b/tests/test_cli/test_set_build_volume.py
@@ -47,19 +47,31 @@ class TestSetBuildVolume(utils.CliTestCase):
         # Case 2. wrong volume
         session.getVolume.return_value = {}
         expected = "No such volume: %s" % volume + "\n"
-        self.assertEqual(
-            1, handle_set_build_volume(options, session, [volume, build]))
-        self.assert_console_message(stdout, expected)
-        activate_session_mock.assert_not_called()
+        self.assert_system_exit(
+                handle_set_build_volume,
+                options,
+                session,
+                [volume, build],
+                exit_code=1,
+                stdout='',
+                stderr=expected,
+                activate_session=None)
 
         # Case 3. no build found
         session.getVolume.return_value = volinfo
         session.getBuild.return_value = {}
-        expected = "No such build: %s" % build + "\n"
-        expected += "No builds to move" + "\n"
-        self.assertEqual(
-            1, handle_set_build_volume(options, session, [volume, build]))
-        self.assert_console_message(stdout, expected)
+        stdout_exp = "No such build: %s" % build + "\n"
+        stderr_exp = "No builds to move" + "\n"
+        self.assert_system_exit(
+                handle_set_build_volume,
+                options,
+                session,
+                [volume, build],
+                exit_code=1,
+                stdout=stdout_exp,
+                stderr=stderr_exp,
+                activate_session=None)
+
 
         # Case 3. Build already in volume
         session.getBuild.side_effect = [
@@ -74,12 +86,19 @@ class TestSetBuildVolume(utils.CliTestCase):
             },
         ]
 
-        expected = "Build %s already on volume %s" % \
+        stdout_exp = "Build %s already on volume %s" % \
             (build, volinfo['name']) + "\n"
-        expected += "No builds to move" + "\n"
-        self.assertEqual(
-            1, handle_set_build_volume(options, session, [volume, build]))
-        self.assert_console_message(stdout, expected)
+        stderr_exp = "No builds to move" + "\n"
+        self.assert_system_exit(
+                handle_set_build_volume,
+                options,
+                session,
+                [volume, build],
+                exit_code=1,
+                stdout=stdout_exp,
+                stderr=stderr_exp,
+                activate_session=None)
+
 
         # Case 4. Change build volume
         build = "sed-4.4-1.fc26"
diff --git a/tests/test_cli/test_wait_repo.py b/tests/test_cli/test_wait_repo.py
index 09e41c9..a73776f 100644
--- a/tests/test_cli/test_wait_repo.py
+++ b/tests/test_cli/test_wait_repo.py
@@ -61,19 +61,39 @@ class TestWaitRepo(utils.CliTestCase):
 
     @mock.patch('time.time')
     @mock.patch('sys.stdout', new_callable=six.StringIO)
-    def __test_wait_repo(self, args, expected, stdout, time_mock):
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
+    def __test_wait_repo(self, args, expected, stderr, stdout, time_mock, ret_code=0):
         self.options.quiet = False
         time_mock.side_effect = [0, 1, 2, 3]
-        anon_handle_wait_repo(self.options, self.session, args)
-        self.assert_console_message(stdout, expected)
+        if ret_code:
+            with self.assertRaises(SystemExit) as ex:
+                anon_handle_wait_repo(self.options, self.session, args)
+            self.assertExitCode(ex, ret_code)
+            self.assert_console_message(stderr, expected)
+            self.assert_console_message(stdout, '')
+        else:
+            rv = anon_handle_wait_repo(self.options, self.session, args)
+            self.assert_console_message(stdout, expected)
+            self.assert_console_message(stderr, '')
+            self.assertIn(rv, [0, None])
 
     @mock.patch('time.time')
     @mock.patch('sys.stdout', new_callable=six.StringIO)
-    def __test_wait_repo_timeout(self, args, expected, stdout, time_mock):
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
+    def __test_wait_repo_timeout(self, args, expected, stderr, stdout, time_mock, ret_code=0):
         self.options.quiet = False
         time_mock.side_effect = [0, 61, 62]
-        anon_handle_wait_repo(self.options, self.session, args + ['--timeout', '1'])
-        self.assert_console_message(stdout, expected)
+        if ret_code:
+            with self.assertRaises(SystemExit) as ex:
+                anon_handle_wait_repo(self.options, self.session, args + ['--timeout', '1'])
+            self.assertExitCode(ex, ret_code)
+            self.assert_console_message(stderr, expected)
+            self.assert_console_message(stdout, '')
+        else:
+            rv = anon_handle_wait_repo(self.options, self.session, args + ['--timeout', '1'])
+            self.assert_console_message(stdout, expected)
+            self.assert_console_message(stderr, '')
+            self.assertIn(rv, [0, None])
 
     def test_anon_handle_wait_repo(self):
         """Test anon_handle_wait_repo function"""
@@ -102,7 +122,7 @@ class TestWaitRepo(utils.CliTestCase):
         self.session.getRepo.return_value = {}
         self.checkForBuilds.return_value = True
         expected = 'Unsuccessfully waited 1:02 for a new %s repo' % self.tag_name + '\n'
-        self.__test_wait_repo_timeout(arguments, expected)
+        self.__test_wait_repo_timeout(arguments, expected, ret_code=1)
 
     def test_anon_handle_wait_repo_with_build(self):
         """Test anon_handle_wait_repo function with --build options"""
@@ -147,7 +167,7 @@ class TestWaitRepo(utils.CliTestCase):
         self.checkForBuilds.return_value = True
         self.session.getRepo.return_value = {}
         expected = 'Unsuccessfully waited 1:02 for %s to appear in the %s repo' % (pkgs, self.tag_name) + '\n'
-        self.__test_wait_repo_timeout(arguments, expected)
+        self.__test_wait_repo_timeout(arguments, expected, ret_code=1)
 
     def test_anon_handle_wait_repo_errors(self):
         """Test anon_handle_wait_repo function errors and exceptions"""
@@ -172,17 +192,18 @@ class TestWaitRepo(utils.CliTestCase):
                 activate_session=None)
         self.activate_session.assert_not_called()
 
-    @mock.patch('sys.stdout', new_callable=six.StringIO)
-    def test_anon_handle_wait_repo_target_not_found(self, stdout):
+    @mock.patch('sys.stderr', new_callable=six.StringIO)
+    def test_anon_handle_wait_repo_target_not_found(self, stderr):
         """Test anon_handle_wait_repo function on target not found cases"""
 
         # Case 1. both build and dest targets are not found
         self.session.getTag.return_value = self.TAG.copy()
         self.session.getBuildTargets.return_value = []
-        rv = anon_handle_wait_repo(self.options, self.session, [self.tag_name])
-        self.assertEqual(rv, 1)
+        with self.assertRaises(SystemExit) as ex:
+            anon_handle_wait_repo(self.options, self.session, [self.tag_name])
+        self.assertExitCode(ex, 1)
         expected = "%(name)s is not a build tag for any target" % self.TAG + "\n"
-        self.assert_console_message(stdout, expected)
+        self.assert_console_message(stderr, expected)
 
         # Cas 2. dest is matched, show suggestion
         self.session.getBuildTargets.side_effect = [
@@ -193,11 +214,12 @@ class TestWaitRepo(utils.CliTestCase):
                 {'build_tag_name': 'build-tag-3'},
             ],
         ]
-        rv = anon_handle_wait_repo(self.options, self.session, [self.tag_name])
-        self.assertEqual(rv, 1)
+        with self.assertRaises(SystemExit) as ex:
+            anon_handle_wait_repo(self.options, self.session, [self.tag_name])
+        self.assertExitCode(ex, 1)
         expected = "%(name)s is not a build tag for any target" % self.TAG + "\n"
         expected += "Suggested tags: build-tag-1, build-tag-2, build-tag-3\n"
-        self.assert_console_message(stdout, expected)
+        self.assert_console_message(stderr, expected)
 
     def test_anon_handle_wait_repo_help(self):
         """Test anon_handle_wait_repo help message"""
diff --git a/tests/test_cli/utils.py b/tests/test_cli/utils.py
index d1c2c9b..31bb4f6 100644
--- a/tests/test_cli/utils.py
+++ b/tests/test_cli/utils.py
@@ -184,17 +184,13 @@ class CliTestCase(unittest.TestCase):
         with session_patch as session:
             with stdout_patch as stdout:
                 with stderr_patch as stderr:
-                    with self.assertRaises(SystemExit) as cm:
+                    with self.assertRaises(SystemExit) as ex:
                         callableObj(*test_args, **test_kwargs)
+                    self.assertExitCode(ex, exit_code)
         session.assert_called_once()
         self.assert_console_message(stdout, **message['stdout'])
         self.assert_console_message(stderr, **message['stderr'])
         assert_function()
-        if isinstance(cm.exception, int):
-            # python 2.6.6
-            self.assertEqual(cm.exception, exit_code)
-        else:
-            self.assertEqual(cm.exception.code, exit_code)
 
     @mock.patch('koji_cli.commands.activate_session')
     def assert_help(self, callableObj, message, activate_session_mock):
@@ -212,6 +208,12 @@ class CliTestCase(unittest.TestCase):
             exit_code=0)
         activate_session_mock.assert_not_called()
 
+    def assertExitCode(self, ex, code):
+        if isinstance(ex.exception, int):
+            self.assertEqual(ex.exception, code)
+        else:
+            self.assertEqual(ex.exception.code, code)
+
 
 def get_builtin_open():
     if six.PY2:

