← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/responses into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/responses into lp:launchpad.

Commit message:
Port from httmock to responses.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/responses/+merge/347193

For a long time I'd thought these two were much of a muchness and it didn't really matter which we used.  However, when porting the external bug tracker code to requests, I found that httmock has a significant flaw: it doesn't support hooks (http://docs.python-requests.org/en/master/user/advanced/#event-hooks).  While that could be patched in, it happens because httmock patches itself in one level higher than responses (at the session level rather than at the adapter level), and so has to duplicate a certain amount of the internal logic of requests.  As such it seems to me that responses has a better design, and it's worth switching to it.  I thought it best to just do this in one shot to keep things tidy.

This isn't particularly mechanical because the two libraries have quite different APIs, but it's test-only and introduces no functional changes.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/responses into lp:launchpad.
=== modified file 'constraints.txt'
--- constraints.txt	2018-05-28 10:16:28 +0000
+++ constraints.txt	2018-05-31 10:48:11 +0000
@@ -239,6 +239,7 @@
 Chameleon==2.11
 chardet==3.0.4
 constantly==15.1.0
+cookies==2.2.1
 cryptography==2.1.4
 cssselect==0.9.1
 cssutils==0.9.10
@@ -258,7 +259,6 @@
 FormEncode==1.2.4
 grokcore.component==1.6
 html5browser==0.0.9
-httmock==1.2.3
 httplib2==0.8
 hyperlink==18.0.0
 idna==2.6
@@ -337,6 +337,7 @@
 rabbitfixture==0.3.6
 requests==2.7.0
 requests-toolbelt==0.6.2
+responses==0.9.0
 scandir==1.7
 service-identity==17.0.0
 setproctitle==1.1.7

=== modified file 'lib/lp/bugs/externalbugtracker/tests/test_github.py'
--- lib/lp/bugs/externalbugtracker/tests/test_github.py	2016-12-15 06:53:13 +0000
+++ lib/lp/bugs/externalbugtracker/tests/test_github.py	2018-05-31 10:48:11 +0000
@@ -1,4 +1,4 @@
-# Copyright 2016 Canonical Ltd.  This software is licensed under the
+# Copyright 2016-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for the GitHub Issues BugTracker."""
@@ -9,16 +9,22 @@
 
 from datetime import datetime
 import json
-from urlparse import (
+
+import pytz
+import responses
+from six.moves.urllib_parse import (
     parse_qs,
+    urlsplit,
     urlunsplit,
     )
-
-from httmock import (
-    HTTMock,
-    urlmatch,
+from testtools.matchers import (
+    Contains,
+    ContainsDict,
+    Equals,
+    MatchesListwise,
+    MatchesStructure,
+    Not,
     )
-import pytz
 import transaction
 from zope.component import getUtility
 
@@ -49,6 +55,14 @@
     )
 
 
+def _add_rate_limit_response(host, limit=5000, remaining=4000,
+                             reset=1000000000):
+    limits = {"limit": limit, "remaining": remaining, "reset": reset}
+    responses.add(
+        "GET", "https://%s/rate_limit"; % host,
+        json={"resources": {"core": limits}})
+
+
 class TestGitHubRateLimit(TestCase):
 
     layer = ZopelessLayer
@@ -58,77 +72,61 @@
         self.rate_limit = getUtility(IGitHubRateLimit)
         self.addCleanup(self.rate_limit.clearCache)
 
-    @urlmatch(path=r"^/rate_limit$")
-    def _rate_limit_handler(self, url, request):
-        self.rate_limit_request = request
-        self.rate_limit_headers = request.headers
-        return {
-            "status_code": 200,
-            "content": {"resources": {"core": self.initial_rate_limit}},
-            }
-
-    @urlmatch(path=r"^/$")
-    def _target_handler(self, url, request):
-        self.target_request = request
-        return {"status_code": 200, "content": b"test"}
-
+    @responses.activate
     def test_makeRequest_no_token(self):
-        self.initial_rate_limit = {
-            "limit": 60, "remaining": 50, "reset": 1000000000}
-        with HTTMock(self._rate_limit_handler, self._target_handler):
-            response = self.rate_limit.makeRequest(
-                "GET", "http://example.org/";)
-        self.assertNotIn("Authorization", self.rate_limit_headers)
+        _add_rate_limit_response("example.org", limit=60, remaining=50)
+        responses.add("GET", "http://example.org/";, body="test")
+        response = self.rate_limit.makeRequest("GET", "http://example.org/";)
+        self.assertThat(responses.calls[0].request, MatchesStructure(
+            path_url=Equals("/rate_limit"),
+            headers=Not(Contains("Authorization"))))
         self.assertEqual(b"test", response.content)
         limit = self.rate_limit._limits[("example.org", None)]
         self.assertEqual(49, limit["remaining"])
         self.assertEqual(1000000000, limit["reset"])
 
         limit["remaining"] = 0
-        self.rate_limit_request = None
-        with HTTMock(self._rate_limit_handler, self._target_handler):
-            self.assertRaisesWithContent(
-                GitHubExceededRateLimit,
-                "Rate limit for example.org exceeded "
-                "(resets at Sun Sep  9 07:16:40 2001)",
-                self.rate_limit.makeRequest,
-                "GET", "http://example.org/";)
-        self.assertIsNone(self.rate_limit_request)
+        responses.reset()
+        self.assertRaisesWithContent(
+            GitHubExceededRateLimit,
+            "Rate limit for example.org exceeded "
+            "(resets at Sun Sep  9 07:16:40 2001)",
+            self.rate_limit.makeRequest,
+            "GET", "http://example.org/";)
+        self.assertEqual(0, len(responses.calls))
         self.assertEqual(0, limit["remaining"])
 
+    @responses.activate
     def test_makeRequest_check_token(self):
-        self.initial_rate_limit = {
-            "limit": 5000, "remaining": 4000, "reset": 1000000000}
-        with HTTMock(self._rate_limit_handler, self._target_handler):
-            response = self.rate_limit.makeRequest(
-                "GET", "http://example.org/";, token="abc")
-        self.assertEqual("token abc", self.rate_limit_headers["Authorization"])
+        _add_rate_limit_response("example.org")
+        responses.add("GET", "http://example.org/";, body="test")
+        response = self.rate_limit.makeRequest(
+            "GET", "http://example.org/";, token="abc")
+        self.assertThat(responses.calls[0].request, MatchesStructure(
+            path_url=Equals("/rate_limit"),
+            headers=ContainsDict({"Authorization": Equals("token abc")})))
         self.assertEqual(b"test", response.content)
         limit = self.rate_limit._limits[("example.org", "abc")]
         self.assertEqual(3999, limit["remaining"])
         self.assertEqual(1000000000, limit["reset"])
 
         limit["remaining"] = 0
-        self.rate_limit_request = None
-        with HTTMock(self._rate_limit_handler, self._target_handler):
-            self.assertRaisesWithContent(
-                GitHubExceededRateLimit,
-                "Rate limit for example.org exceeded "
-                "(resets at Sun Sep  9 07:16:40 2001)",
-                self.rate_limit.makeRequest,
-                "GET", "http://example.org/";, token="abc")
-        self.assertIsNone(self.rate_limit_request)
+        responses.reset()
+        self.assertRaisesWithContent(
+            GitHubExceededRateLimit,
+            "Rate limit for example.org exceeded "
+            "(resets at Sun Sep  9 07:16:40 2001)",
+            self.rate_limit.makeRequest,
+            "GET", "http://example.org/";, token="abc")
+        self.assertEqual(0, len(responses.calls))
         self.assertEqual(0, limit["remaining"])
 
+    @responses.activate
     def test_makeRequest_check_503(self):
-        @urlmatch(path=r"^/rate_limit$")
-        def rate_limit_handler(url, request):
-            return {"status_code": 503}
-
-        with HTTMock(rate_limit_handler):
-            self.assertRaises(
-                BugTrackerConnectError, self.rate_limit.makeRequest,
-                "GET", "http://example.org/";)
+        responses.add("GET", "https://example.org/rate_limit";, status=503)
+        self.assertRaises(
+            BugTrackerConnectError, self.rate_limit.makeRequest,
+            "GET", "http://example.org/";)
 
 
 class TestGitHub(TestCase):
@@ -156,105 +154,108 @@
         self.assertRaises(
             BadGitHubURL, GitHub, "https://github.com/user/repository";)
 
-    @urlmatch(path=r"^/rate_limit$")
-    def _rate_limit_handler(self, url, request):
-        self.rate_limit_request = request
-        rate_limit = {"limit": 5000, "remaining": 4000, "reset": 1000000000}
-        return {
-            "status_code": 200,
-            "content": {"resources": {"core": rate_limit}},
-            }
-
+    @responses.activate
     def test__getPage_authenticated(self):
-        @urlmatch(path=r".*/test$")
-        def handler(url, request):
-            self.request = request
-            return {"status_code": 200, "content": json.dumps("success")}
-
+        _add_rate_limit_response("api.github.com")
+        responses.add(
+            "GET", "https://api.github.com/repos/user/repository/test";,
+            json="success")
         self.pushConfig(
             "checkwatches.credentials", **{"api.github.com.token": "sosekrit"})
         tracker = GitHub("https://github.com/user/repository/issues";)
-        with HTTMock(self._rate_limit_handler, handler):
-            self.assertEqual("success", tracker._getPage("test").json())
-        self.assertEqual(
-            "https://api.github.com/repos/user/repository/test";,
-            self.request.url)
-        self.assertEqual(
-            "token sosekrit", self.request.headers["Authorization"])
-        self.assertEqual(
-            "token sosekrit", self.rate_limit_request.headers["Authorization"])
+        self.assertEqual("success", tracker._getPage("test").json())
+        requests = [call.request for call in responses.calls]
+        self.assertThat(requests, MatchesListwise([
+            MatchesStructure(
+                path_url=Equals("/rate_limit"),
+                headers=ContainsDict({
+                    "Authorization": Equals("token sosekrit"),
+                    })),
+            MatchesStructure(
+                path_url=Equals("/repos/user/repository/test"),
+                headers=ContainsDict({
+                    "Authorization": Equals("token sosekrit"),
+                    })),
+            ]))
 
+    @responses.activate
     def test__getPage_unauthenticated(self):
-        @urlmatch(path=r".*/test$")
-        def handler(url, request):
-            self.request = request
-            return {"status_code": 200, "content": json.dumps("success")}
-
+        _add_rate_limit_response("api.github.com")
+        responses.add(
+            "GET", "https://api.github.com/repos/user/repository/test";,
+            json="success")
         tracker = GitHub("https://github.com/user/repository/issues";)
-        with HTTMock(self._rate_limit_handler, handler):
-            self.assertEqual("success", tracker._getPage("test").json())
-        self.assertEqual(
-            "https://api.github.com/repos/user/repository/test";,
-            self.request.url)
-        self.assertNotIn("Authorization", self.request.headers)
-        self.assertNotIn("Authorization", self.rate_limit_request.headers)
+        self.assertEqual("success", tracker._getPage("test").json())
+        requests = [call.request for call in responses.calls]
+        self.assertThat(requests, MatchesListwise([
+            MatchesStructure(
+                path_url=Equals("/rate_limit"),
+                headers=Not(Contains("Authorization"))),
+            MatchesStructure(
+                path_url=Equals("/repos/user/repository/test"),
+                headers=Not(Contains("Authorization"))),
+            ]))
 
+    @responses.activate
     def test_getRemoteBug(self):
-        @urlmatch(path=r".*/issues/1$")
-        def handler(url, request):
-            self.request = request
-            return {"status_code": 200, "content": self.sample_bugs[0]}
-
+        _add_rate_limit_response("api.github.com")
+        responses.add(
+            "GET", "https://api.github.com/repos/user/repository/issues/1";,
+            json=self.sample_bugs[0])
         tracker = GitHub("https://github.com/user/repository/issues";)
-        with HTTMock(self._rate_limit_handler, handler):
-            self.assertEqual(
-                (1, self.sample_bugs[0]), tracker.getRemoteBug("1"))
+        self.assertEqual((1, self.sample_bugs[0]), tracker.getRemoteBug("1"))
         self.assertEqual(
             "https://api.github.com/repos/user/repository/issues/1";,
-            self.request.url)
-
-    @urlmatch(path=r".*/issues$")
-    def _issues_handler(self, url, request):
-        self.issues_request = request
-        return {"status_code": 200, "content": json.dumps(self.sample_bugs)}
-
+            responses.calls[-1].request.url)
+
+    def _addIssuesResponse(self):
+        responses.add(
+            "GET", "https://api.github.com/repos/user/repository/issues";,
+            json=self.sample_bugs)
+
+    @responses.activate
     def test_getRemoteBugBatch(self):
+        _add_rate_limit_response("api.github.com")
+        self._addIssuesResponse()
         tracker = GitHub("https://github.com/user/repository/issues";)
-        with HTTMock(self._rate_limit_handler, self._issues_handler):
-            self.assertEqual(
-                {bug["id"]: bug for bug in self.sample_bugs[:2]},
-                tracker.getRemoteBugBatch(["1", "2"]))
+        self.assertEqual(
+            {bug["id"]: bug for bug in self.sample_bugs[:2]},
+            tracker.getRemoteBugBatch(["1", "2"]))
         self.assertEqual(
             "https://api.github.com/repos/user/repository/issues?state=all";,
-            self.issues_request.url)
+            responses.calls[-1].request.url)
 
+    @responses.activate
     def test_getRemoteBugBatch_last_accessed(self):
+        _add_rate_limit_response("api.github.com")
+        self._addIssuesResponse()
         tracker = GitHub("https://github.com/user/repository/issues";)
         since = datetime(2015, 1, 1, 12, 0, 0, tzinfo=pytz.UTC)
-        with HTTMock(self._rate_limit_handler, self._issues_handler):
-            self.assertEqual(
-                {bug["id"]: bug for bug in self.sample_bugs[:2]},
-                tracker.getRemoteBugBatch(["1", "2"], last_accessed=since))
+        self.assertEqual(
+            {bug["id"]: bug for bug in self.sample_bugs[:2]},
+            tracker.getRemoteBugBatch(["1", "2"], last_accessed=since))
         self.assertEqual(
             "https://api.github.com/repos/user/repository/issues?";
             "state=all&since=2015-01-01T12%3A00%3A00Z",
-            self.issues_request.url)
+            responses.calls[-1].request.url)
 
+    @responses.activate
     def test_getRemoteBugBatch_caching(self):
+        _add_rate_limit_response("api.github.com")
+        self._addIssuesResponse()
         tracker = GitHub("https://github.com/user/repository/issues";)
-        with HTTMock(self._rate_limit_handler, self._issues_handler):
-            tracker.initializeRemoteBugDB(
-                [str(bug["id"]) for bug in self.sample_bugs])
-            self.issues_request = None
-            self.assertEqual(
-                {bug["id"]: bug for bug in self.sample_bugs[:2]},
-                tracker.getRemoteBugBatch(["1", "2"]))
-            self.assertIsNone(self.issues_request)
+        tracker.initializeRemoteBugDB(
+            [str(bug["id"]) for bug in self.sample_bugs])
+        responses.reset()
+        self.assertEqual(
+            {bug["id"]: bug for bug in self.sample_bugs[:2]},
+            tracker.getRemoteBugBatch(["1", "2"]))
+        self.assertEqual(0, len(responses.calls))
 
+    @responses.activate
     def test_getRemoteBugBatch_pagination(self):
-        @urlmatch(path=r".*/issues")
-        def handler(url, request):
-            self.issues_requests.append(request)
+        def issues_callback(request):
+            url = urlsplit(request.url)
             base_url = urlunsplit(list(url[:3]) + ["", ""])
             page = int(parse_qs(url.query).get("page", ["1"])[0])
             links = []
@@ -266,27 +267,29 @@
                 links.append('<%s?page=%d>; rel="prev"' % (base_url, page - 1))
             start = (page - 1) * 2
             end = page * 2
-            return {
-                "status_code": 200,
-                "headers": {"Link": ", ".join(links)},
-                "content": json.dumps(self.sample_bugs[start:end]),
-                }
+            return (
+                200, {"Link": ", ".join(links)},
+                json.dumps(self.sample_bugs[start:end]))
 
-        self.issues_requests = []
+        _add_rate_limit_response("api.github.com")
+        responses.add_callback(
+            "GET", "https://api.github.com/repos/user/repository/issues";,
+            callback=issues_callback, content_type="application/json")
         tracker = GitHub("https://github.com/user/repository/issues";)
-        with HTTMock(self._rate_limit_handler, handler):
-            self.assertEqual(
-                {bug["id"]: bug for bug in self.sample_bugs},
-                tracker.getRemoteBugBatch(
-                    [str(bug["id"]) for bug in self.sample_bugs]))
+        self.assertEqual(
+            {bug["id"]: bug for bug in self.sample_bugs},
+            tracker.getRemoteBugBatch(
+                [str(bug["id"]) for bug in self.sample_bugs]))
         expected_urls = [
+            "https://api.github.com/rate_limit";,
             "https://api.github.com/repos/user/repository/issues?state=all";,
             "https://api.github.com/repos/user/repository/issues?page=2";,
             "https://api.github.com/repos/user/repository/issues?page=3";,
             ]
         self.assertEqual(
-            expected_urls, [request.url for request in self.issues_requests])
+            expected_urls, [call.request.url for call in responses.calls])
 
+    @responses.activate
     def test_status_open(self):
         self.sample_bugs = [
             {"id": 1, "state": "open", "labels": []},
@@ -294,9 +297,10 @@
             {"id": 2, "state": "open",
              "labels": [{"name": "feature"}, {"name": "closed"}]},
             ]
+        _add_rate_limit_response("api.github.com")
+        self._addIssuesResponse()
         tracker = GitHub("https://github.com/user/repository/issues";)
-        with HTTMock(self._rate_limit_handler, self._issues_handler):
-            tracker.initializeRemoteBugDB(["1", "2"])
+        tracker.initializeRemoteBugDB(["1", "2"])
         remote_status = tracker.getRemoteStatus("1")
         self.assertEqual("open", remote_status)
         lp_status = tracker.convertRemoteStatus(remote_status)
@@ -306,6 +310,7 @@
         lp_status = tracker.convertRemoteStatus(remote_status)
         self.assertEqual(BugTaskStatus.NEW, lp_status)
 
+    @responses.activate
     def test_status_closed(self):
         self.sample_bugs = [
             {"id": 1, "state": "closed", "labels": []},
@@ -313,9 +318,10 @@
             {"id": 2, "state": "closed",
              "labels": [{"name": "feature"}, {"name": "open"}]},
             ]
+        _add_rate_limit_response("api.github.com")
+        self._addIssuesResponse()
         tracker = GitHub("https://github.com/user/repository/issues";)
-        with HTTMock(self._rate_limit_handler, self._issues_handler):
-            tracker.initializeRemoteBugDB(["1", "2"])
+        tracker.initializeRemoteBugDB(["1", "2"])
         remote_status = tracker.getRemoteStatus("1")
         self.assertEqual("closed", remote_status)
         lp_status = tracker.convertRemoteStatus(remote_status)
@@ -330,22 +336,13 @@
 
     layer = ZopelessDatabaseLayer
 
-    @urlmatch(path=r"^/rate_limit$")
-    def _rate_limit_handler(self, url, request):
-        self.rate_limit_request = request
-        rate_limit = {"limit": 5000, "remaining": 4000, "reset": 1000000000}
-        return {
-            "status_code": 200,
-            "content": {"resources": {"core": rate_limit}},
-            }
-
+    @responses.activate
     def test_process_one(self):
         remote_bug = {"id": 1234, "state": "open", "labels": []}
-
-        @urlmatch(path=r".*/issues/1234$")
-        def handler(url, request):
-            return {"status_code": 200, "content": remote_bug}
-
+        _add_rate_limit_response("api.github.com")
+        responses.add(
+            "GET", "https://api.github.com/repos/user/repository/issues/1234";,
+            json=remote_bug)
         bug = self.factory.makeBug()
         bug_tracker = self.factory.makeBugTracker(
             base_url="https://github.com/user/repository/issues";,
@@ -360,8 +357,7 @@
         logger = BufferLogger()
         bug_watch_updater = CheckwatchesMaster(transaction, logger=logger)
         github = get_external_bugtracker(bug_tracker)
-        with HTTMock(self._rate_limit_handler, handler):
-            bug_watch_updater.updateBugWatches(github, bug_tracker.watches)
+        bug_watch_updater.updateBugWatches(github, bug_tracker.watches)
         self.assertEqual(
             "INFO Updating 1 watches for 1 bugs on "
             "https://api.github.com/repos/user/repository\n";,
@@ -371,17 +367,17 @@
             [(watch.remotebug, github.convertRemoteStatus(watch.remotestatus))
              for watch in bug_tracker.watches])
 
+    @responses.activate
     def test_process_many(self):
         remote_bugs = [
             {"id": bug_id,
              "state": "open" if (bug_id % 2) == 0 else "closed",
              "labels": []}
             for bug_id in range(1000, 1010)]
-
-        @urlmatch(path=r".*/issues$")
-        def handler(url, request):
-            return {"status_code": 200, "content": json.dumps(remote_bugs)}
-
+        _add_rate_limit_response("api.github.com")
+        responses.add(
+            "GET", "https://api.github.com/repos/user/repository/issues";,
+            json=remote_bugs)
         bug = self.factory.makeBug()
         bug_tracker = self.factory.makeBugTracker(
             base_url="https://github.com/user/repository/issues";,
@@ -394,8 +390,7 @@
         logger = BufferLogger()
         bug_watch_updater = CheckwatchesMaster(transaction, logger=logger)
         github = get_external_bugtracker(bug_tracker)
-        with HTTMock(self._rate_limit_handler, handler):
-            bug_watch_updater.updateBugWatches(github, bug_tracker.watches)
+        bug_watch_updater.updateBugWatches(github, bug_tracker.watches)
         self.assertEqual(
             "INFO Updating 10 watches for 10 bugs on "
             "https://api.github.com/repos/user/repository\n";,

=== modified file 'lib/lp/code/model/tests/test_githosting.py'
--- lib/lp/code/model/tests/test_githosting.py	2018-03-31 13:30:36 +0000
+++ lib/lp/code/model/tests/test_githosting.py	2018-05-31 10:48:11 +0000
@@ -17,12 +17,10 @@
 
 from contextlib import contextmanager
 import json
+import re
 
-from httmock import (
-    all_requests,
-    HTTMock,
-    )
 from lazr.restful.utils import get_current_browser_request
+import responses
 from testtools.matchers import MatchesStructure
 from zope.component import getUtility
 from zope.interface import implementer
@@ -62,22 +60,12 @@
         super(TestGitHostingClient, self).setUp()
         self.client = getUtility(IGitHostingClient)
         self.endpoint = removeSecurityProxy(self.client).endpoint
-        self.request = None
+        self.requests = []
 
     @contextmanager
-    def mockRequests(self, status_code=200, content=b"", reason=None,
-                     set_default_timeout=True):
-        @all_requests
-        def handler(url, request):
-            self.assertIsNone(self.request)
-            self.request = request
-            return {
-                "status_code": status_code,
-                "content": content,
-                "reason": reason,
-                }
-
-        with HTTMock(handler):
+    def mockRequests(self, method, set_default_timeout=True, **kwargs):
+        with responses.RequestsMock() as requests_mock:
+            requests_mock.add(method, re.compile(r".*"), **kwargs)
             original_timeout_function = get_default_timeout_function()
             if set_default_timeout:
                 set_default_timeout_function(lambda: 60.0)
@@ -85,12 +73,14 @@
                 yield
             finally:
                 set_default_timeout_function(original_timeout_function)
+            self.requests = [call.request for call in requests_mock.calls]
 
     def assertRequest(self, url_suffix, json_data=None, method=None, **kwargs):
-        self.assertThat(self.request, MatchesStructure.byEquality(
+        [request] = self.requests
+        self.assertThat(request, MatchesStructure.byEquality(
             url=urlappend(self.endpoint, url_suffix), method=method, **kwargs))
         if json_data is not None:
-            self.assertEqual(json_data, json.loads(self.request.body))
+            self.assertEqual(json_data, json.loads(request.body))
         timeline = get_request_timeline(get_current_browser_request())
         action = timeline.actions[-1]
         self.assertEqual("git-hosting-%s" % method.lower(), action.category)
@@ -98,94 +88,94 @@
             "/" + url_suffix.split("?", 1)[0], action.detail.split(" ", 1)[0])
 
     def test_create(self):
-        with self.mockRequests():
+        with self.mockRequests("POST"):
             self.client.create("123")
         self.assertRequest(
             "repo", method="POST", json_data={"repo_path": "123"})
 
     def test_create_clone_from(self):
-        with self.mockRequests():
+        with self.mockRequests("POST"):
             self.client.create("123", clone_from="122")
         self.assertRequest(
             "repo", method="POST",
             json_data={"repo_path": "123", "clone_from": "122"})
 
     def test_create_failure(self):
-        with self.mockRequests(status_code=400, reason=b"Bad request"):
+        with self.mockRequests("POST", status=400):
             self.assertRaisesWithContent(
                 GitRepositoryCreationFault,
                 "Failed to create Git repository: "
-                "400 Client Error: Bad request",
+                "400 Client Error: Bad Request",
                 self.client.create, "123")
 
     def test_getProperties(self):
         with self.mockRequests(
-                content=b'{"default_branch": "refs/heads/master"}'):
+                "GET", json={"default_branch": "refs/heads/master"}):
             props = self.client.getProperties("123")
         self.assertEqual({"default_branch": "refs/heads/master"}, props)
         self.assertRequest("repo/123", method="GET")
 
     def test_getProperties_failure(self):
-        with self.mockRequests(status_code=400, reason=b"Bad request"):
+        with self.mockRequests("GET", status=400):
             self.assertRaisesWithContent(
                 GitRepositoryScanFault,
                 "Failed to get properties of Git repository: "
-                "400 Client Error: Bad request",
+                "400 Client Error: Bad Request",
                 self.client.getProperties, "123")
 
     def test_setProperties(self):
-        with self.mockRequests():
+        with self.mockRequests("PATCH"):
             self.client.setProperties("123", default_branch="refs/heads/a")
         self.assertRequest(
             "repo/123", method="PATCH",
             json_data={"default_branch": "refs/heads/a"})
 
     def test_setProperties_failure(self):
-        with self.mockRequests(status_code=400, reason=b"Bad request"):
+        with self.mockRequests("PATCH", status=400):
             self.assertRaisesWithContent(
                 GitRepositoryScanFault,
                 "Failed to set properties of Git repository: "
-                "400 Client Error: Bad request",
+                "400 Client Error: Bad Request",
                 self.client.setProperties, "123",
                 default_branch="refs/heads/a")
 
     def test_getRefs(self):
-        with self.mockRequests(content=b'{"refs/heads/master": {}}'):
+        with self.mockRequests("GET", json={"refs/heads/master": {}}):
             refs = self.client.getRefs("123")
         self.assertEqual({"refs/heads/master": {}}, refs)
         self.assertRequest("repo/123/refs", method="GET")
 
     def test_getRefs_failure(self):
-        with self.mockRequests(status_code=400, reason=b"Bad request"):
+        with self.mockRequests("GET", status=400):
             self.assertRaisesWithContent(
                 GitRepositoryScanFault,
                 "Failed to get refs from Git repository: "
-                "400 Client Error: Bad request",
+                "400 Client Error: Bad Request",
                 self.client.getRefs, "123")
 
     def test_getCommits(self):
-        with self.mockRequests(content=b'[{"sha1": "0"}]'):
+        with self.mockRequests("POST", json=[{"sha1": "0"}]):
             commits = self.client.getCommits("123", ["0"])
         self.assertEqual([{"sha1": "0"}], commits)
         self.assertRequest(
             "repo/123/commits", method="POST", json_data={"commits": ["0"]})
 
     def test_getCommits_failure(self):
-        with self.mockRequests(status_code=400, reason=b"Bad request"):
+        with self.mockRequests("POST", status=400):
             self.assertRaisesWithContent(
                 GitRepositoryScanFault,
                 "Failed to get commit details from Git repository: "
-                "400 Client Error: Bad request",
+                "400 Client Error: Bad Request",
                 self.client.getCommits, "123", ["0"])
 
     def test_getLog(self):
-        with self.mockRequests(content=b'[{"sha1": "0"}]'):
+        with self.mockRequests("GET", json=[{"sha1": "0"}]):
             log = self.client.getLog("123", "refs/heads/master")
         self.assertEqual([{"sha1": "0"}], log)
         self.assertRequest("repo/123/log/refs/heads/master", method="GET")
 
     def test_getLog_limit_stop(self):
-        with self.mockRequests(content=b'[{"sha1": "0"}]'):
+        with self.mockRequests("GET", json=[{"sha1": "0"}]):
             log = self.client.getLog(
                 "123", "refs/heads/master", limit=10, stop="refs/heads/old")
         self.assertEqual([{"sha1": "0"}], log)
@@ -194,48 +184,48 @@
             method="GET")
 
     def test_getLog_failure(self):
-        with self.mockRequests(status_code=400, reason=b"Bad request"):
+        with self.mockRequests("GET", status=400):
             self.assertRaisesWithContent(
                 GitRepositoryScanFault,
                 "Failed to get commit log from Git repository: "
-                "400 Client Error: Bad request",
+                "400 Client Error: Bad Request",
                 self.client.getLog, "123", "refs/heads/master")
 
     def test_getDiff(self):
-        with self.mockRequests(content=b'{"patch": ""}'):
+        with self.mockRequests("GET", json={"patch": ""}):
             diff = self.client.getDiff("123", "a", "b")
         self.assertEqual({"patch": ""}, diff)
         self.assertRequest("repo/123/compare/a..b", method="GET")
 
     def test_getDiff_common_ancestor(self):
-        with self.mockRequests(content=b'{"patch": ""}'):
+        with self.mockRequests("GET", json={"patch": ""}):
             diff = self.client.getDiff("123", "a", "b", common_ancestor=True)
         self.assertEqual({"patch": ""}, diff)
         self.assertRequest("repo/123/compare/a...b", method="GET")
 
     def test_getDiff_context_lines(self):
-        with self.mockRequests(content=b'{"patch": ""}'):
+        with self.mockRequests("GET", json={"patch": ""}):
             diff = self.client.getDiff("123", "a", "b", context_lines=4)
         self.assertEqual({"patch": ""}, diff)
         self.assertRequest(
             "repo/123/compare/a..b?context_lines=4", method="GET")
 
     def test_getDiff_failure(self):
-        with self.mockRequests(status_code=400, reason=b"Bad request"):
+        with self.mockRequests("GET", status=400):
             self.assertRaisesWithContent(
                 GitRepositoryScanFault,
                 "Failed to get diff from Git repository: "
-                "400 Client Error: Bad request",
+                "400 Client Error: Bad Request",
                 self.client.getDiff, "123", "a", "b")
 
     def test_getMergeDiff(self):
-        with self.mockRequests(content=b'{"patch": ""}'):
+        with self.mockRequests("GET", json={"patch": ""}):
             diff = self.client.getMergeDiff("123", "a", "b")
         self.assertEqual({"patch": ""}, diff)
         self.assertRequest("repo/123/compare-merge/a:b", method="GET")
 
     def test_getMergeDiff_prerequisite(self):
-        with self.mockRequests(content=b'{"patch": ""}'):
+        with self.mockRequests("GET", json={"patch": ""}):
             diff = self.client.getMergeDiff("123", "a", "b", prerequisite="c")
         self.assertEqual({"patch": ""}, diff)
         self.assertRequest(
@@ -245,23 +235,23 @@
         # pygit2 tries to decode the diff as UTF-8 with errors="replace".
         # In some cases this can result in unpaired surrogates, which older
         # versions of json/simplejson don't like.
-        content = json.dumps(
+        body = json.dumps(
             {"patch": "卷。".encode("GBK").decode("UTF-8", errors="replace")})
-        with self.mockRequests(content=content):
+        with self.mockRequests("GET", body=body):
             diff = self.client.getMergeDiff("123", "a", "b")
         self.assertEqual({"patch": "\uFFFD\uD863"}, diff)
         self.assertRequest("repo/123/compare-merge/a:b", method="GET")
 
     def test_getMergeDiff_failure(self):
-        with self.mockRequests(status_code=400, reason=b"Bad request"):
+        with self.mockRequests("GET", status=400):
             self.assertRaisesWithContent(
                 GitRepositoryScanFault,
                 "Failed to get merge diff from Git repository: "
-                "400 Client Error: Bad request",
+                "400 Client Error: Bad Request",
                 self.client.getMergeDiff, "123", "a", "b")
 
     def test_detectMerges(self):
-        with self.mockRequests(content=b'{"b": "0"}'):
+        with self.mockRequests("POST", json={"b": "0"}):
             merges = self.client.detectMerges("123", "a", ["b", "c"])
         self.assertEqual({"b": "0"}, merges)
         self.assertRequest(
@@ -269,30 +259,30 @@
             json_data={"sources": ["b", "c"]})
 
     def test_detectMerges_failure(self):
-        with self.mockRequests(status_code=400, reason=b"Bad request"):
+        with self.mockRequests("POST", status=400):
             self.assertRaisesWithContent(
                 GitRepositoryScanFault,
                 "Failed to detect merges in Git repository: "
-                "400 Client Error: Bad request",
+                "400 Client Error: Bad Request",
                 self.client.detectMerges, "123", "a", ["b", "c"])
 
     def test_delete(self):
-        with self.mockRequests():
+        with self.mockRequests("DELETE"):
             self.client.delete("123")
         self.assertRequest("repo/123", method="DELETE")
 
     def test_delete_failed(self):
-        with self.mockRequests(status_code=400, reason=b"Bad request"):
+        with self.mockRequests("DELETE", status=400):
             self.assertRaisesWithContent(
                 GitRepositoryDeletionFault,
                 "Failed to delete Git repository: "
-                "400 Client Error: Bad request",
+                "400 Client Error: Bad Request",
                 self.client.delete, "123")
 
     def test_getBlob(self):
         blob = b''.join(chr(i) for i in range(256))
-        content = {"data": blob.encode("base64"), "size": len(blob)}
-        with self.mockRequests(content=json.dumps(content)):
+        payload = {"data": blob.encode("base64"), "size": len(blob)}
+        with self.mockRequests("GET", json=payload):
             response = self.client.getBlob("123", "dir/path/file/name")
         self.assertEqual(blob, response)
         self.assertRequest(
@@ -300,22 +290,22 @@
 
     def test_getBlob_revision(self):
         blob = b''.join(chr(i) for i in range(256))
-        content = {"data": blob.encode("base64"), "size": len(blob)}
-        with self.mockRequests(content=json.dumps(content)):
+        payload = {"data": blob.encode("base64"), "size": len(blob)}
+        with self.mockRequests("GET", json=payload):
             response = self.client.getBlob("123", "dir/path/file/name", "dev")
         self.assertEqual(blob, response)
         self.assertRequest(
             "repo/123/blob/dir/path/file/name?rev=dev", method="GET")
 
     def test_getBlob_not_found(self):
-        with self.mockRequests(status_code=404, reason=b"Not found"):
+        with self.mockRequests("GET", status=404):
             self.assertRaisesWithContent(
                 GitRepositoryBlobNotFound,
                 "Repository 123 has no file dir/path/file/name",
                 self.client.getBlob, "123", "dir/path/file/name")
 
     def test_getBlob_revision_not_found(self):
-        with self.mockRequests(status_code=404, reason=b"Not found"):
+        with self.mockRequests("GET", status=404):
             self.assertRaisesWithContent(
                 GitRepositoryBlobNotFound,
                 "Repository 123 has no file dir/path/file/name "
@@ -323,39 +313,38 @@
                 self.client.getBlob, "123", "dir/path/file/name", "dev")
 
     def test_getBlob_failure(self):
-        with self.mockRequests(status_code=400, reason=b"Bad request"):
+        with self.mockRequests("GET", status=400):
             self.assertRaisesWithContent(
                 GitRepositoryScanFault,
                 "Failed to get file from Git repository: "
-                "400 Client Error: Bad request",
+                "400 Client Error: Bad Request",
                 self.client.getBlob, "123", "dir/path/file/name")
 
     def test_getBlob_url_quoting(self):
         blob = b''.join(chr(i) for i in range(256))
-        content = {"data": blob.encode("base64"), "size": len(blob)}
-        with self.mockRequests(content=json.dumps(content)):
+        payload = {"data": blob.encode("base64"), "size": len(blob)}
+        with self.mockRequests("GET", json=payload):
             self.client.getBlob("123", "dir/+file name?.txt", "+rev/ no?")
         self.assertRequest(
             "repo/123/blob/dir/%2Bfile%20name%3F.txt?rev=%2Brev%2F+no%3F",
             method="GET")
 
     def test_getBlob_no_data(self):
-        with self.mockRequests(content=json.dumps({"size": 1})):
+        with self.mockRequests("GET", json={"size": 1}):
             self.assertRaisesWithContent(
                 GitRepositoryScanFault,
                 "Failed to get file from Git repository: 'data'",
                 self.client.getBlob, "123", "dir/path/file/name")
 
     def test_getBlob_no_size(self):
-        with self.mockRequests(content=json.dumps({"data": "data"})):
+        with self.mockRequests("GET", json={"data": "data"}):
             self.assertRaisesWithContent(
                 GitRepositoryScanFault,
                 "Failed to get file from Git repository: 'size'",
                 self.client.getBlob, "123", "dir/path/file/name")
 
     def test_getBlob_bad_encoding(self):
-        content = {"data": "x", "size": 1}
-        with self.mockRequests(content=json.dumps(content)):
+        with self.mockRequests("GET", json={"data": "x", "size": 1}):
             self.assertRaisesWithContent(
                 GitRepositoryScanFault,
                 "Failed to get file from Git repository: Incorrect padding",
@@ -363,8 +352,8 @@
 
     def test_getBlob_wrong_size(self):
         blob = b''.join(chr(i) for i in range(256))
-        content = {"data": blob.encode("base64"), "size": 0}
-        with self.mockRequests(content=json.dumps(content)):
+        payload = {"data": blob.encode("base64"), "size": 0}
+        with self.mockRequests("GET", json=payload):
             self.assertRaisesWithContent(
                 GitRepositoryScanFault,
                 "Failed to get file from Git repository: Unexpected size"
@@ -382,7 +371,7 @@
 
             def run(self):
                 with self.testcase.mockRequests(
-                        content=b'{"refs/heads/master": {}}',
+                        "GET", json={"refs/heads/master": {}},
                         set_default_timeout=False):
                     self.refs = self.testcase.client.getRefs("123")
                 # We must make this assertion inside the job, since the job

=== modified file 'lib/lp/services/webhooks/tests/test_job.py'
--- lib/lp/services/webhooks/tests/test_job.py	2017-03-28 23:56:03 +0000
+++ lib/lp/services/webhooks/tests/test_job.py	2018-05-31 10:48:11 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015-2017 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for `WebhookJob`s."""
@@ -9,14 +9,12 @@
     datetime,
     timedelta,
     )
+import re
 
-from httmock import (
-    HTTMock,
-    urlmatch,
-    )
 from pytz import utc
 import requests
 import requests.exceptions
+import responses
 from storm.store import Store
 from testtools import TestCase
 from testtools.matchers import (
@@ -139,25 +137,18 @@
 class TestWebhookClient(TestCase):
     """Tests for `WebhookClient`."""
 
-    def sendToWebhook(self, response_status=200, raises=None, headers=None):
-        reqs = []
-
-        @urlmatch(netloc='example.com')
-        def endpoint_mock(url, request):
-            if raises:
-                raise raises
-            reqs.append(request)
-            return {
-                'status_code': response_status, 'content': 'Content',
-                'headers': headers}
-
-        with HTTMock(endpoint_mock):
+    def sendToWebhook(self, body='Content', **kwargs):
+        with responses.RequestsMock() as requests_mock:
+            requests_mock.add(
+                'POST', re.compile('^http://example\.com/'), body=body,
+                **kwargs)
             result = WebhookClient().deliver(
                 'http://example.com/ep', 'http://squid.example.com:3128',
                 'TestWebhookClient', 30, 'sekrit', '1234', 'test',
                 {'foo': 'bar'})
+            calls = list(requests_mock.calls)
 
-        return reqs, result
+        return calls, result
 
     @property
     def request_matcher(self):
@@ -177,27 +168,27 @@
             })
 
     def test_sends_request(self):
-        [request], result = self.sendToWebhook()
+        [call], result = self.sendToWebhook()
         self.assertThat(
             result,
             MatchesDict({
                 'request': self.request_matcher,
                 'response': MatchesDict({
                     'status_code': Equals(200),
-                    'headers': Equals({}),
+                    'headers': Equals({'content-type': 'text/plain'}),
                     'body': Equals('Content'),
                     }),
                 }))
 
     def test_accepts_404(self):
-        [request], result = self.sendToWebhook(response_status=404)
+        [call], result = self.sendToWebhook(status=404)
         self.assertThat(
             result,
             MatchesDict({
                 'request': self.request_matcher,
                 'response': MatchesDict({
                     'status_code': Equals(404),
-                    'headers': Equals({}),
+                    'headers': Equals({'content-type': 'text/plain'}),
                     'body': Equals('Content'),
                     }),
                 }))
@@ -205,35 +196,34 @@
     def test_connection_error(self):
         # Attempts that fail to connect have a connection_error rather
         # than a response.
-        reqs, result = self.sendToWebhook(
-            raises=requests.ConnectionError('Connection refused'))
+        [call], result = self.sendToWebhook(
+            body=requests.ConnectionError('Connection refused'))
         self.assertThat(
             result,
             MatchesDict({
                 'request': self.request_matcher,
                 'connection_error': Equals('Connection refused'),
                 }))
-        self.assertEqual([], reqs)
+        self.assertIsInstance(call.response, requests.ConnectionError)
 
     def test_timeout_error(self):
         # Attempts that don't return within the timeout have a
         # connection_error rather than a response.
-        reqs, result = self.sendToWebhook(
-            raises=requests.exceptions.ReadTimeout())
+        [call], result = self.sendToWebhook(
+            body=requests.exceptions.ReadTimeout())
         self.assertThat(
             result,
             MatchesDict({
                 'request': self.request_matcher,
                 'connection_error': Equals('Request timeout'),
                 }))
-        self.assertEqual([], reqs)
+        self.assertIsInstance(call.response, requests.exceptions.ReadTimeout)
 
     def test_proxy_error_known(self):
         # Squid error headers are interpreted to populate
         # connection_error.
-        [request], result = self.sendToWebhook(
-            response_status=403,
-            headers={"X-Squid-Error": "ERR_ACCESS_DENIED 0"})
+        [call], result = self.sendToWebhook(
+            status=403, headers={"X-Squid-Error": "ERR_ACCESS_DENIED 0"})
         self.assertThat(
             result,
             MatchesDict({
@@ -244,9 +234,8 @@
     def test_proxy_error_unknown(self):
         # Squid errors that don't have a human-readable mapping are
         # included verbatim.
-        [request], result = self.sendToWebhook(
-            response_status=403,
-            headers={"X-Squid-Error": "ERR_BORKED 1234"})
+        [call], result = self.sendToWebhook(
+            status=403, headers={"X-Squid-Error": "ERR_BORKED 1234"})
         self.assertThat(
             result,
             MatchesDict({

=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py	2018-05-07 05:25:27 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py	2018-05-31 10:48:11 +0000
@@ -20,14 +20,11 @@
     )
 
 from fixtures import FakeLogger
-from httmock import (
-    all_requests,
-    HTTMock,
-    )
 from mechanize import LinkNotFoundError
 import mock
 from pymacaroons import Macaroon
 import pytz
+import responses
 import soupmatchers
 from testtools.matchers import (
     MatchesSetwise,
@@ -403,6 +400,7 @@
             "Pocket for automatic builds:\nSecurity\nEdit snap package",
             MatchesTagText(content, "auto_build_pocket"))
 
+    @responses.activate
     def test_create_new_snap_store_upload(self):
         # Creating a new snap and asking for it to be automatically uploaded
         # to the store sets all the appropriate fields and redirects to SSO
@@ -422,19 +420,12 @@
             urlsplit(config.launchpad.openid_provider_root).netloc, "",
             "dummy")
         root_macaroon_raw = root_macaroon.serialize()
-
-        @all_requests
-        def handler(url, request):
-            self.request = request
-            return {
-                "status_code": 200,
-                "content": {"macaroon": root_macaroon_raw},
-                }
-
         self.pushConfig("snappy", store_url="http://sca.example/";)
-        with HTTMock(handler):
-            redirection = self.assertRaises(
-                HTTPError, browser.getControl("Create snap package").click)
+        responses.add(
+            "POST", "http://sca.example/dev/api/acl/";,
+            json={"macaroon": root_macaroon_raw})
+        redirection = self.assertRaises(
+            HTTPError, browser.getControl("Create snap package").click)
         login_person(self.person)
         snap = getUtility(ISnapSet).getByName(self.person, "snap-name")
         self.assertThat(snap, MatchesStructure.byEquality(
@@ -443,7 +434,8 @@
             store_series=self.snappyseries, store_name="store-name",
             store_secrets={"root": root_macaroon_raw},
             store_channels=["track/edge"]))
-        self.assertThat(self.request, MatchesStructure.byEquality(
+        [call] = responses.calls
+        self.assertThat(call.request, MatchesStructure.byEquality(
             url="http://sca.example/dev/api/acl/";, method="POST"))
         expected_body = {
             "packages": [{
@@ -452,7 +444,7 @@
                 }],
             "permissions": ["package_upload"],
             }
-        self.assertEqual(expected_body, json.loads(self.request.body))
+        self.assertEqual(expected_body, json.loads(call.request.body))
         self.assertEqual(303, redirection.code)
         parsed_location = urlsplit(redirection.hdrs["Location"])
         self.assertEqual(
@@ -969,6 +961,7 @@
         self.assertNeedStoreReauth(
             True, {"store_upload": False}, {"store_upload": True})
 
+    @responses.activate
     def test_edit_store_upload(self):
         # Changing store upload settings on a snap sets all the appropriate
         # fields and redirects to SSO for reauthorization.
@@ -989,30 +982,24 @@
             urlsplit(config.launchpad.openid_provider_root).netloc, "",
             "dummy")
         root_macaroon_raw = root_macaroon.serialize()
-
-        @all_requests
-        def handler(url, request):
-            self.request = request
-            return {
-                "status_code": 200,
-                "content": {"macaroon": root_macaroon_raw},
-                }
-
         self.pushConfig("snappy", store_url="http://sca.example/";)
-        with HTTMock(handler):
-            redirection = self.assertRaises(
-                HTTPError, browser.getControl("Update snap package").click)
+        responses.add(
+            "POST", "http://sca.example/dev/api/acl/";,
+            json={"macaroon": root_macaroon_raw})
+        redirection = self.assertRaises(
+            HTTPError, browser.getControl("Update snap package").click)
         login_person(self.person)
         self.assertThat(snap, MatchesStructure.byEquality(
             store_name="two", store_secrets={"root": root_macaroon_raw},
             store_channels=["stable", "edge"]))
-        self.assertThat(self.request, MatchesStructure.byEquality(
+        [call] = responses.calls
+        self.assertThat(call.request, MatchesStructure.byEquality(
             url="http://sca.example/dev/api/acl/";, method="POST"))
         expected_body = {
             "packages": [{"name": "two", "series": self.snappyseries.name}],
             "permissions": ["package_upload"],
             }
-        self.assertEqual(expected_body, json.loads(self.request.body))
+        self.assertEqual(expected_body, json.loads(call.request.body))
         self.assertEqual(303, redirection.code)
         parsed_location = urlsplit(redirection.hdrs["Location"])
         self.assertEqual(
@@ -1047,6 +1034,7 @@
             Unauthorized, self.getUserBrowser,
             canonical_url(self.snap) + "/+authorize", user=other_person)
 
+    @responses.activate
     def test_begin_authorization(self):
         # With no special form actions, we return a form inviting the user
         # to begin authorization.  This allows (re-)authorizing uploads of
@@ -1058,22 +1046,16 @@
             urlsplit(config.launchpad.openid_provider_root).netloc, '',
             'dummy')
         root_macaroon_raw = root_macaroon.serialize()
-
-        @all_requests
-        def handler(url, request):
-            self.request = request
-            return {
-                "status_code": 200,
-                "content": {"macaroon": root_macaroon_raw},
-                }
-
         self.pushConfig("snappy", store_url="http://sca.example/";)
-        with HTTMock(handler):
-            browser = self.getNonRedirectingBrowser(
-                url=snap_url + "/+authorize", user=self.snap.owner)
-            redirection = self.assertRaises(
-                HTTPError, browser.getControl("Begin authorization").click)
-        self.assertThat(self.request, MatchesStructure.byEquality(
+        responses.add(
+            "POST", "http://sca.example/dev/api/acl/";,
+            json={"macaroon": root_macaroon_raw})
+        browser = self.getNonRedirectingBrowser(
+            url=snap_url + "/+authorize", user=self.snap.owner)
+        redirection = self.assertRaises(
+            HTTPError, browser.getControl("Begin authorization").click)
+        [call] = responses.calls
+        self.assertThat(call.request, MatchesStructure.byEquality(
             url="http://sca.example/dev/api/acl/";, method="POST"))
         with person_logged_in(owner):
             expected_body = {
@@ -1083,7 +1065,7 @@
                     }],
                 "permissions": ["package_upload"],
                 }
-            self.assertEqual(expected_body, json.loads(self.request.body))
+            self.assertEqual(expected_body, json.loads(call.request.body))
             self.assertEqual(
                 {"root": root_macaroon_raw}, self.snap.store_secrets)
         self.assertEqual(303, redirection.code)

=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py	2018-04-21 10:01:22 +0000
+++ lib/lp/snappy/tests/test_snap.py	2018-05-31 10:48:11 +0000
@@ -14,13 +14,10 @@
 import json
 from urlparse import urlsplit
 
-from httmock import (
-    all_requests,
-    HTTMock,
-    )
 from lazr.lifecycle.event import ObjectModifiedEvent
 from pymacaroons import Macaroon
 import pytz
+import responses
 from storm.exceptions import LostObjectError
 from storm.locals import Store
 from testtools.matchers import (
@@ -1919,21 +1916,16 @@
             urlsplit(config.launchpad.openid_provider_root).netloc, '',
             'dummy')
         root_macaroon_raw = root_macaroon.serialize()
-
-        @all_requests
-        def handler(url, request):
-            self.request = request
-            return {
-                "status_code": 200,
-                "content": {"macaroon": root_macaroon_raw},
-                }
-
         self.pushConfig("snappy", store_url="http://sca.example/";)
         logout()
-        with HTTMock(handler):
+        with responses.RequestsMock() as requests_mock:
+            requests_mock.add(
+                "POST", "http://sca.example/dev/api/acl/";,
+                json={"macaroon": root_macaroon_raw})
             response = self.webservice.named_post(
                 snap_url, "beginAuthorization", **kwargs)
-        self.assertThat(self.request, MatchesStructure.byEquality(
+            [call] = requests_mock.calls
+        self.assertThat(call.request, MatchesStructure.byEquality(
             url="http://sca.example/dev/api/acl/";, method="POST"))
         with person_logged_in(self.person):
             expected_body = {
@@ -1943,7 +1935,7 @@
                     }],
                 "permissions": ["package_upload"],
                 }
-            self.assertEqual(expected_body, json.loads(self.request.body))
+            self.assertEqual(expected_body, json.loads(call.request.body))
             self.assertEqual({"root": root_macaroon_raw}, snap.store_secrets)
         return response, root_macaroon.third_party_caveats()[0]
 

=== modified file 'lib/lp/snappy/tests/test_snapstoreclient.py'
--- lib/lp/snappy/tests/test_snapstoreclient.py	2018-05-02 23:55:51 +0000
+++ lib/lp/snappy/tests/test_snapstoreclient.py	2018-05-31 10:48:11 +0000
@@ -13,11 +13,6 @@
 import io
 import json
 
-from httmock import (
-    all_requests,
-    HTTMock,
-    urlmatch,
-    )
 from lazr.restful.utils import get_current_browser_request
 from pymacaroons import (
     Macaroon,
@@ -25,6 +20,7 @@
     )
 from requests import Request
 from requests.utils import parse_dict_header
+import responses
 from testtools.matchers import (
     Contains,
     ContainsDict,
@@ -231,6 +227,7 @@
             {"name": "stable", "display_name": "Stable"},
             {"name": "edge", "display_name": "Edge"},
             ]
+        self.channels_memcache_key = "search.example:channels".encode("UTF-8")
 
     def _make_store_secrets(self):
         self.root_key = hashlib.sha256(
@@ -249,51 +246,45 @@
             "discharge": unbound_discharge_macaroon.serialize(),
             }
 
-    @urlmatch(path=r".*/unscanned-upload/$")
-    def _unscanned_upload_handler(self, url, request):
-        self.unscanned_upload_requests.append(request)
-        return {
-            "status_code": 200,
-            "content": {"successful": True, "upload_id": 1},
-            }
+    def _addUnscannedUploadResponse(self):
+        responses.add(
+            "POST", "http://updown.example/unscanned-upload/";,
+            json={"successful": True, "upload_id": 1})
 
-    @urlmatch(path=r".*/snap-push/$")
-    def _snap_push_handler(self, url, request):
-        self.snap_push_request = request
-        return {
-            "status_code": 202,
-            "content": {
+    def _addSnapPushResponse(self):
+        responses.add(
+            "POST", "http://sca.example/dev/api/snap-push/";, status=202,
+            json={
                 "success": True,
                 "status_details_url": (
                     "http://sca.example/dev/api/snaps/1/builds/1/status";),
-                }}
-
-    @urlmatch(path=r".*/api/v2/tokens/refresh$")
-    def _macaroon_refresh_handler(self, url, request):
-        self.refresh_request = request
-        new_macaroon = Macaroon(
-            location="sso.example", key=self.discharge_key,
-            identifier=self.discharge_caveat_id)
-        new_macaroon.add_first_party_caveat("sso|expires|tomorrow")
-        return {
-            "status_code": 200,
-            "content": {"discharge_macaroon": new_macaroon.serialize()},
-            }
-
-    @urlmatch(path=r".*/api/v1/channels$")
-    def _channels_handler(self, url, request):
-        self.channels_request = request
-        return {
-            "status_code": 200,
-            "content": {"_embedded": {"clickindex:channel": self.channels}},
-            }
-
-    @urlmatch(path=r".*/snap-release/$")
-    def _snap_release_handler(self, url, request):
-        self.snap_release_request = request
-        return {
-            "status_code": 200,
-            "content": {
+                })
+
+    def _addMacaroonRefreshResponse(self):
+        def callback(request):
+            new_macaroon = Macaroon(
+                location="sso.example", key=self.discharge_key,
+                identifier=self.discharge_caveat_id)
+            new_macaroon.add_first_party_caveat("sso|expires|tomorrow")
+            return (
+                200, {},
+                json.dumps({"discharge_macaroon": new_macaroon.serialize()}))
+
+        responses.add_callback(
+            "POST", "http://sso.example/api/v2/tokens/refresh";,
+            callback=callback, content_type="application/json")
+
+    def _addChannelsResponse(self):
+        responses.add(
+            "GET", "http://search.example/api/v1/channels";,
+            json={"_embedded": {"clickindex:channel": self.channels}})
+        self.addCleanup(
+            getUtility(IMemcacheClient).delete, self.channels_memcache_key)
+
+    def _addSnapReleaseResponse(self):
+        responses.add(
+            "POST", "http://sca.example/dev/api/snap-release/";,
+            json={
                 "success": True,
                 "channel_map": [
                     {"channel": "stable", "info": "specific",
@@ -302,19 +293,17 @@
                      "version": "1.0", "revision": 1},
                     ],
                 "opened_channels": ["stable", "edge"],
-                }}
+                })
 
+    @responses.activate
     def test_requestPackageUploadPermission(self):
-        @all_requests
-        def handler(url, request):
-            self.request = request
-            return {"status_code": 200, "content": {"macaroon": "dummy"}}
-
         snappy_series = self.factory.makeSnappySeries(name="rolling")
-        with HTTMock(handler):
-            macaroon = self.client.requestPackageUploadPermission(
-                snappy_series, "test-snap")
-        self.assertThat(self.request, RequestMatches(
+        responses.add(
+            "POST", "http://sca.example/dev/api/acl/";,
+            json={"macaroon": "dummy"})
+        macaroon = self.client.requestPackageUploadPermission(
+            snappy_series, "test-snap")
+        self.assertThat(responses.calls[-1].request, RequestMatches(
             url=Equals("http://sca.example/dev/api/acl/";),
             method=Equals("POST"),
             json_data={
@@ -329,45 +318,35 @@
         self.assertEqual("request-snap-upload-macaroon-stop", stop.category)
         self.assertEqual("rolling/test-snap", stop.detail)
 
+    @responses.activate
     def test_requestPackageUploadPermission_missing_macaroon(self):
-        @all_requests
-        def handler(url, request):
-            return {"status_code": 200, "content": {}}
-
         snappy_series = self.factory.makeSnappySeries()
-        with HTTMock(handler):
-            self.assertRaisesWithContent(
-                BadRequestPackageUploadResponse, b"{}",
-                self.client.requestPackageUploadPermission,
-                snappy_series, "test-snap")
+        responses.add("POST", "http://sca.example/dev/api/acl/";, json={})
+        self.assertRaisesWithContent(
+            BadRequestPackageUploadResponse, b"{}",
+            self.client.requestPackageUploadPermission,
+            snappy_series, "test-snap")
 
+    @responses.activate
     def test_requestPackageUploadPermission_error(self):
-        @all_requests
-        def handler(url, request):
-            return {
-                "status_code": 503,
-                "content": {"error_list": [{"message": "Failed"}]},
-                }
-
         snappy_series = self.factory.makeSnappySeries()
-        with HTTMock(handler):
-            self.assertRaisesWithContent(
-                BadRequestPackageUploadResponse, "Failed",
-                self.client.requestPackageUploadPermission,
-                snappy_series, "test-snap")
+        responses.add(
+            "POST", "http://sca.example/dev/api/acl/";,
+            status=503, json={"error_list": [{"message": "Failed"}]})
+        self.assertRaisesWithContent(
+            BadRequestPackageUploadResponse, "Failed",
+            self.client.requestPackageUploadPermission,
+            snappy_series, "test-snap")
 
+    @responses.activate
     def test_requestPackageUploadPermission_404(self):
-        @all_requests
-        def handler(url, request):
-            return {"status_code": 404, "reason": b"Not found"}
-
         snappy_series = self.factory.makeSnappySeries()
-        with HTTMock(handler):
-            self.assertRaisesWithContent(
-                BadRequestPackageUploadResponse,
-                b"404 Client Error: Not found",
-                self.client.requestPackageUploadPermission,
-                snappy_series, "test-snap")
+        responses.add("POST", "http://sca.example/dev/api/acl/";, status=404)
+        self.assertRaisesWithContent(
+            BadRequestPackageUploadResponse,
+            b"404 Client Error: Not Found",
+            self.client.requestPackageUploadPermission,
+            snappy_series, "test-snap")
 
     def makeUploadableSnapBuild(self, store_secrets=None):
         if store_secrets is None:
@@ -386,16 +365,18 @@
             snapbuild=snapbuild, libraryfile=manifest_lfa)
         return snapbuild
 
+    @responses.activate
     def test_upload(self):
         snapbuild = self.makeUploadableSnapBuild()
         transaction.commit()
+        self._addUnscannedUploadResponse()
+        self._addSnapPushResponse()
         with dbuser(config.ISnapStoreUploadJobSource.dbuser):
-            with HTTMock(self._unscanned_upload_handler,
-                         self._snap_push_handler):
-                self.assertEqual(
-                    "http://sca.example/dev/api/snaps/1/builds/1/status";,
-                    self.client.upload(snapbuild))
-        self.assertThat(self.unscanned_upload_requests, MatchesListwise([
+            self.assertEqual(
+                "http://sca.example/dev/api/snaps/1/builds/1/status";,
+                self.client.upload(snapbuild))
+        requests = [call.request for call in responses.calls]
+        self.assertThat(requests, MatchesListwise([
             RequestMatches(
                 url=Equals("http://updown.example/unscanned-upload/";),
                 method=Equals("POST"),
@@ -404,29 +385,33 @@
                         name="binary", filename="test-snap.snap",
                         value="dummy snap content",
                         type="application/octet-stream",
-                        )})]))
-        self.assertThat(self.snap_push_request, RequestMatches(
-            url=Equals("http://sca.example/dev/api/snap-push/";),
-            method=Equals("POST"),
-            headers=ContainsDict({"Content-Type": Equals("application/json")}),
-            auth=("Macaroon", MacaroonsVerify(self.root_key)),
-            json_data={
-                "name": "test-snap", "updown_id": 1, "series": "rolling",
-                }))
+                        )}),
+            RequestMatches(
+                url=Equals("http://sca.example/dev/api/snap-push/";),
+                method=Equals("POST"),
+                headers=ContainsDict(
+                    {"Content-Type": Equals("application/json")}),
+                auth=("Macaroon", MacaroonsVerify(self.root_key)),
+                json_data={
+                    "name": "test-snap", "updown_id": 1, "series": "rolling",
+                    }),
+            ]))
 
+    @responses.activate
     def test_upload_no_discharge(self):
         root_key = hashlib.sha256(self.factory.getUniqueString()).hexdigest()
         root_macaroon = Macaroon(key=root_key)
         snapbuild = self.makeUploadableSnapBuild(
             store_secrets={"root": root_macaroon.serialize()})
         transaction.commit()
+        self._addUnscannedUploadResponse()
+        self._addSnapPushResponse()
         with dbuser(config.ISnapStoreUploadJobSource.dbuser):
-            with HTTMock(self._unscanned_upload_handler,
-                         self._snap_push_handler):
-                self.assertEqual(
-                    "http://sca.example/dev/api/snaps/1/builds/1/status";,
-                    self.client.upload(snapbuild))
-        self.assertThat(self.unscanned_upload_requests, MatchesListwise([
+            self.assertEqual(
+                "http://sca.example/dev/api/snaps/1/builds/1/status";,
+                self.client.upload(snapbuild))
+        requests = [call.request for call in responses.calls]
+        self.assertThat(requests, MatchesListwise([
             RequestMatches(
                 url=Equals("http://updown.example/unscanned-upload/";),
                 method=Equals("POST"),
@@ -435,126 +420,106 @@
                         name="binary", filename="test-snap.snap",
                         value="dummy snap content",
                         type="application/octet-stream",
-                        )})]))
-        self.assertThat(self.snap_push_request, RequestMatches(
-            url=Equals("http://sca.example/dev/api/snap-push/";),
-            method=Equals("POST"),
-            headers=ContainsDict({"Content-Type": Equals("application/json")}),
-            auth=("Macaroon", MacaroonsVerify(root_key)),
-            json_data={
-                "name": "test-snap", "updown_id": 1, "series": "rolling",
-                }))
+                        )}),
+            RequestMatches(
+                url=Equals("http://sca.example/dev/api/snap-push/";),
+                method=Equals("POST"),
+                headers=ContainsDict(
+                    {"Content-Type": Equals("application/json")}),
+                auth=("Macaroon", MacaroonsVerify(root_key)),
+                json_data={
+                    "name": "test-snap", "updown_id": 1, "series": "rolling",
+                    }),
+            ]))
 
+    @responses.activate
     def test_upload_unauthorized(self):
-        @urlmatch(path=r".*/snap-push/$")
-        def snap_push_handler(url, request):
-            self.snap_push_request = request
-            return {
-                "status_code": 401,
-                "headers": {"WWW-Authenticate": 'Macaroon realm="Devportal"'},
-                "content": {
-                    "error_list": [{
-                        "code": "macaroon-permission-required",
-                        "message": "Permission is required: package_push",
-                        }],
-                    },
-                }
-
         store_secrets = self._make_store_secrets()
         snapbuild = self.makeUploadableSnapBuild(store_secrets=store_secrets)
         transaction.commit()
+        self._addUnscannedUploadResponse()
+        snap_push_error = {
+            "code": "macaroon-permission-required",
+            "message": "Permission is required: package_push",
+            }
+        responses.add(
+            "POST", "http://sca.example/dev/api/snap-push/";, status=401,
+            headers={"WWW-Authenticate": 'Macaroon realm="Devportal"'},
+            json={"error_list": [snap_push_error]})
         with dbuser(config.ISnapStoreUploadJobSource.dbuser):
-            with HTTMock(self._unscanned_upload_handler, snap_push_handler,
-                         self._macaroon_refresh_handler):
-                self.assertRaisesWithContent(
-                    UnauthorizedUploadResponse,
-                    "Permission is required: package_push",
-                    self.client.upload, snapbuild)
+            self.assertRaisesWithContent(
+                UnauthorizedUploadResponse,
+                "Permission is required: package_push",
+                self.client.upload, snapbuild)
 
+    @responses.activate
     def test_upload_needs_discharge_macaroon_refresh(self):
-        @urlmatch(path=r".*/snap-push/$")
-        def snap_push_handler(url, request):
-            snap_push_handler.call_count += 1
-            if snap_push_handler.call_count == 1:
-                self.first_snap_push_request = request
-                return {
-                    "status_code": 401,
-                    "headers": {
-                        "WWW-Authenticate": "Macaroon needs_refresh=1"}}
-            else:
-                return self._snap_push_handler(url, request)
-        snap_push_handler.call_count = 0
-
         store_secrets = self._make_store_secrets()
         snapbuild = self.makeUploadableSnapBuild(store_secrets=store_secrets)
         transaction.commit()
+        self._addUnscannedUploadResponse()
+        responses.add(
+            "POST", "http://sca.example/dev/api/snap-push/";, status=401,
+            headers={"WWW-Authenticate": "Macaroon needs_refresh=1"})
+        self._addMacaroonRefreshResponse()
+        self._addSnapPushResponse()
         with dbuser(config.ISnapStoreUploadJobSource.dbuser):
-            with HTTMock(self._unscanned_upload_handler, snap_push_handler,
-                         self._macaroon_refresh_handler):
-                self.assertEqual(
-                    "http://sca.example/dev/api/snaps/1/builds/1/status";,
-                    self.client.upload(snapbuild))
-        self.assertEqual(2, snap_push_handler.call_count)
+            self.assertEqual(
+                "http://sca.example/dev/api/snaps/1/builds/1/status";,
+                self.client.upload(snapbuild))
+        requests = [call.request for call in responses.calls]
+        self.assertThat(requests, MatchesListwise([
+            MatchesStructure.byEquality(path_url="/unscanned-upload/"),
+            MatchesStructure.byEquality(path_url="/dev/api/snap-push/"),
+            MatchesStructure.byEquality(path_url="/api/v2/tokens/refresh"),
+            MatchesStructure.byEquality(path_url="/dev/api/snap-push/"),
+            ]))
         self.assertNotEqual(
             store_secrets["discharge"],
             snapbuild.snap.store_secrets["discharge"])
 
+    @responses.activate
     def test_upload_unsigned_agreement(self):
-        @urlmatch(path=r".*/snap-push/$")
-        def snap_push_handler(url, request):
-            self.snap_push_request = request
-            return {
-                "status_code": 403,
-                "content": {
-                    "error_list": [
-                        {"message": "Developer has not signed agreement."},
-                        ],
-                    },
-                }
-
         store_secrets = self._make_store_secrets()
         snapbuild = self.makeUploadableSnapBuild(store_secrets=store_secrets)
         transaction.commit()
+        self._addUnscannedUploadResponse()
+        snap_push_error = {"message": "Developer has not signed agreement."}
+        responses.add(
+            "POST", "http://sca.example/dev/api/snap-push/";, status=403,
+            json={"error_list": [snap_push_error]})
         with dbuser(config.ISnapStoreUploadJobSource.dbuser):
-            with HTTMock(self._unscanned_upload_handler, snap_push_handler,
-                         self._macaroon_refresh_handler):
-                err = self.assertRaises(
-                    UploadFailedResponse, self.client.upload, snapbuild)
-                self.assertEqual(
-                    "Developer has not signed agreement.", str(err))
-                self.assertFalse(err.can_retry)
+            err = self.assertRaises(
+                UploadFailedResponse, self.client.upload, snapbuild)
+            self.assertEqual("Developer has not signed agreement.", str(err))
+            self.assertFalse(err.can_retry)
 
+    @responses.activate
     def test_upload_file_error(self):
-        @urlmatch(path=r".*/unscanned-upload/$")
-        def unscanned_upload_handler(url, request):
-            return {
-                "status_code": 502,
-                "reason": "Proxy Error",
-                "content": b"The proxy exploded.\n",
-                }
-
         store_secrets = self._make_store_secrets()
         snapbuild = self.makeUploadableSnapBuild(store_secrets=store_secrets)
         transaction.commit()
+        responses.add(
+            "POST", "http://updown.example/unscanned-upload/";, status=502,
+            body="The proxy exploded.\n")
         with dbuser(config.ISnapStoreUploadJobSource.dbuser):
-            with HTTMock(unscanned_upload_handler):
-                err = self.assertRaises(
-                    UploadFailedResponse, self.client.upload, snapbuild)
-                self.assertEqual("502 Server Error: Proxy Error", str(err))
-                self.assertEqual(b"The proxy exploded.\n", err.detail)
-                self.assertTrue(err.can_retry)
+            err = self.assertRaises(
+                UploadFailedResponse, self.client.upload, snapbuild)
+            self.assertEqual("502 Server Error: Bad Gateway", str(err))
+            self.assertEqual(b"The proxy exploded.\n", err.detail)
+            self.assertTrue(err.can_retry)
 
+    @responses.activate
     def test_refresh_discharge_macaroon(self):
         store_secrets = self._make_store_secrets()
         snap = self.factory.makeSnap(
             store_upload=True,
             store_series=self.factory.makeSnappySeries(name="rolling"),
             store_name="test-snap", store_secrets=store_secrets)
-
+        self._addMacaroonRefreshResponse()
         with dbuser(config.ISnapStoreUploadJobSource.dbuser):
-            with HTTMock(self._macaroon_refresh_handler):
-                self.client.refreshDischargeMacaroon(snap)
-        self.assertThat(self.refresh_request, RequestMatches(
+            self.client.refreshDischargeMacaroon(snap)
+        self.assertThat(responses.calls[-1].request, RequestMatches(
             url=Equals("http://sso.example/api/v2/tokens/refresh";),
             method=Equals("POST"),
             headers=ContainsDict({"Content-Type": Equals("application/json")}),
@@ -562,127 +527,100 @@
         self.assertNotEqual(
             store_secrets["discharge"], snap.store_secrets["discharge"])
 
+    @responses.activate
     def test_checkStatus_pending(self):
-        @all_requests
-        def handler(url, request):
-            return {
-                "status_code": 200,
-                "content": {
-                    "code": "being_processed", "processed": False,
-                    "can_release": False,
-                    }}
-
         status_url = "http://sca.example/dev/api/snaps/1/builds/1/status";
-        with HTTMock(handler):
-            self.assertRaises(
-                UploadNotScannedYetResponse, self.client.checkStatus,
-                status_url)
+        responses.add(
+            "GET", status_url,
+            json={
+                "code": "being_processed", "processed": False,
+                "can_release": False,
+                })
+        self.assertRaises(
+            UploadNotScannedYetResponse, self.client.checkStatus, status_url)
 
+    @responses.activate
     def test_checkStatus_error(self):
-        @all_requests
-        def handler(url, request):
-            return {
-                "status_code": 200,
-                "content": {
-                    "code": "processing_error", "processed": True,
-                    "can_release": False,
-                    "errors": [
-                        {"code": None,
-                         "message": "You cannot use that reserved namespace.",
-                         "link": "http://example.com";
-                         }],
-                    }}
-
         status_url = "http://sca.example/dev/api/snaps/1/builds/1/status";
-        with HTTMock(handler):
-            self.assertRaisesWithContent(
-                ScanFailedResponse,
-                b"You cannot use that reserved namespace.",
-                self.client.checkStatus, status_url)
+        responses.add(
+            "GET", status_url,
+            json={
+                "code": "processing_error", "processed": True,
+                "can_release": False,
+                "errors": [
+                    {"code": None,
+                     "message": "You cannot use that reserved namespace.",
+                     "link": "http://example.com";
+                     }],
+                })
+        self.assertRaisesWithContent(
+            ScanFailedResponse, b"You cannot use that reserved namespace.",
+            self.client.checkStatus, status_url)
 
+    @responses.activate
     def test_checkStatus_review_error(self):
-        @all_requests
-        def handler(url, request):
-            return {
-                "status_code": 200,
-                "content": {
-                    "code": "processing_error", "processed": True,
-                    "can_release": False,
-                    "errors": [{"code": None, "message": "Review failed."}],
-                    "url": "http://sca.example/dev/click-apps/1/rev/1/";,
-                    }}
-
         status_url = "http://sca.example/dev/api/snaps/1/builds/1/status";
-        with HTTMock(handler):
-            self.assertRaisesWithContent(
-                ScanFailedResponse, b"Review failed.",
-                self.client.checkStatus, status_url)
+        responses.add(
+            "GET", status_url,
+            json={
+                "code": "processing_error", "processed": True,
+                "can_release": False,
+                "errors": [{"code": None, "message": "Review failed."}],
+                "url": "http://sca.example/dev/click-apps/1/rev/1/";,
+                })
+        self.assertRaisesWithContent(
+            ScanFailedResponse, b"Review failed.",
+            self.client.checkStatus, status_url)
 
+    @responses.activate
     def test_checkStatus_complete(self):
-        @all_requests
-        def handler(url, request):
-            return {
-                "status_code": 200,
-                "content": {
-                    "code": "ready_to_release", "processed": True,
-                    "can_release": True,
-                    "url": "http://sca.example/dev/click-apps/1/rev/1/";,
-                    "revision": 1,
-                    }}
-
         status_url = "http://sca.example/dev/api/snaps/1/builds/1/status";
-        with HTTMock(handler):
-            self.assertEqual(
-                ("http://sca.example/dev/click-apps/1/rev/1/";, 1),
-                self.client.checkStatus(status_url))
+        responses.add(
+            "GET", status_url,
+            json={
+                "code": "ready_to_release", "processed": True,
+                "can_release": True,
+                "url": "http://sca.example/dev/click-apps/1/rev/1/";,
+                "revision": 1,
+                })
+        self.assertEqual(
+            ("http://sca.example/dev/click-apps/1/rev/1/";, 1),
+            self.client.checkStatus(status_url))
 
+    @responses.activate
     def test_checkStatus_404(self):
-        @all_requests
-        def handler(url, request):
-            return {"status_code": 404, "reason": b"Not found"}
-
         status_url = "http://sca.example/dev/api/snaps/1/builds/1/status";
-        with HTTMock(handler):
-            self.assertRaisesWithContent(
-                BadScanStatusResponse, b"404 Client Error: Not found",
-                self.client.checkStatus, status_url)
+        responses.add("GET", status_url, status=404)
+        self.assertRaisesWithContent(
+            BadScanStatusResponse, b"404 Client Error: Not Found",
+            self.client.checkStatus, status_url)
 
+    @responses.activate
     def test_listChannels(self):
-        memcache_key = "search.example:channels".encode("UTF-8")
-        try:
-            with HTTMock(self._channels_handler):
-                self.assertEqual(self.channels, self.client.listChannels())
-            self.assertThat(self.channels_request, RequestMatches(
-                url=Equals("http://search.example/api/v1/channels";),
-                method=Equals("GET"),
-                headers=ContainsDict(
-                    {"Accept": Equals("application/hal+json")})))
-            self.assertEqual(
-                self.channels,
-                json.loads(getUtility(IMemcacheClient).get(memcache_key)))
-            self.channels_request = None
-            with HTTMock(self._channels_handler):
-                self.assertEqual(self.channels, self.client.listChannels())
-            self.assertIsNone(self.channels_request)
-        finally:
-            getUtility(IMemcacheClient).delete(memcache_key)
+        self._addChannelsResponse()
+        self.assertEqual(self.channels, self.client.listChannels())
+        self.assertThat(responses.calls[-1].request, RequestMatches(
+            url=Equals("http://search.example/api/v1/channels";),
+            method=Equals("GET"),
+            headers=ContainsDict({"Accept": Equals("application/hal+json")})))
+        self.assertEqual(
+            self.channels,
+            json.loads(getUtility(IMemcacheClient).get(
+                self.channels_memcache_key)))
+        responses.reset()
+        self.assertEqual(self.channels, self.client.listChannels())
+        self.assertContentEqual([], responses.calls)
 
+    @responses.activate
     def test_listChannels_404(self):
-        @all_requests
-        def handler(url, request):
-            return {"status_code": 404, "reason": b"Not found"}
-
-        with HTTMock(handler):
-            self.assertRaisesWithContent(
-                BadSearchResponse, b"404 Client Error: Not found",
-                self.client.listChannels)
-
+        responses.add(
+            "GET", "http://search.example/api/v1/channels";, status=404)
+        self.assertRaisesWithContent(
+            BadSearchResponse, b"404 Client Error: Not Found",
+            self.client.listChannels)
+
+    @responses.activate
     def test_listChannels_disable_search(self):
-        @all_requests
-        def handler(url, request):
-            self.request = request
-            return {"status_code": 404, "reason": b"Not found"}
-
         self.useFixture(
             FeatureFixture({u"snap.disable_channel_search": u"on"}))
         expected_channels = [
@@ -691,25 +629,23 @@
             {"name": "beta", "display_name": "Beta"},
             {"name": "stable", "display_name": "Stable"},
             ]
-        self.request = None
-        with HTTMock(handler):
-            self.assertEqual(expected_channels, self.client.listChannels())
-        self.assertIsNone(self.request)
-        memcache_key = "search.example:channels".encode("UTF-8")
-        self.assertIsNone(getUtility(IMemcacheClient).get(memcache_key))
+        self.assertEqual(expected_channels, self.client.listChannels())
+        self.assertContentEqual([], responses.calls)
+        self.assertIsNone(
+            getUtility(IMemcacheClient).get(self.channels_memcache_key))
 
+    @responses.activate
     def test_release(self):
-        with HTTMock(self._channels_handler):
-            snap = self.factory.makeSnap(
-                store_upload=True,
-                store_series=self.factory.makeSnappySeries(name="rolling"),
-                store_name="test-snap",
-                store_secrets=self._make_store_secrets(),
-                store_channels=["stable", "edge"])
+        snap = self.factory.makeSnap(
+            store_upload=True,
+            store_series=self.factory.makeSnappySeries(name="rolling"),
+            store_name="test-snap",
+            store_secrets=self._make_store_secrets(),
+            store_channels=["stable", "edge"])
         snapbuild = self.factory.makeSnapBuild(snap=snap)
-        with HTTMock(self._snap_release_handler):
-            self.client.release(snapbuild, 1)
-        self.assertThat(self.snap_release_request, RequestMatches(
+        self._addSnapReleaseResponse()
+        self.client.release(snapbuild, 1)
+        self.assertThat(responses.calls[-1].request, RequestMatches(
             url=Equals("http://sca.example/dev/api/snap-release/";),
             method=Equals("POST"),
             headers=ContainsDict({"Content-Type": Equals("application/json")}),
@@ -719,20 +655,20 @@
                 "channels": ["stable", "edge"], "series": "rolling",
                 }))
 
+    @responses.activate
     def test_release_no_discharge(self):
         root_key = hashlib.sha256(self.factory.getUniqueString()).hexdigest()
         root_macaroon = Macaroon(key=root_key)
-        with HTTMock(self._channels_handler):
-            snap = self.factory.makeSnap(
-                store_upload=True,
-                store_series=self.factory.makeSnappySeries(name="rolling"),
-                store_name="test-snap",
-                store_secrets={"root": root_macaroon.serialize()},
-                store_channels=["stable", "edge"])
+        snap = self.factory.makeSnap(
+            store_upload=True,
+            store_series=self.factory.makeSnappySeries(name="rolling"),
+            store_name="test-snap",
+            store_secrets={"root": root_macaroon.serialize()},
+            store_channels=["stable", "edge"])
         snapbuild = self.factory.makeSnapBuild(snap=snap)
-        with HTTMock(self._snap_release_handler):
-            self.client.release(snapbuild, 1)
-        self.assertThat(self.snap_release_request, RequestMatches(
+        self._addSnapReleaseResponse()
+        self.client.release(snapbuild, 1)
+        self.assertThat(responses.calls[-1].request, RequestMatches(
             url=Equals("http://sca.example/dev/api/snap-release/";),
             method=Equals("POST"),
             headers=ContainsDict({"Content-Type": Equals("application/json")}),
@@ -742,69 +678,55 @@
                 "channels": ["stable", "edge"], "series": "rolling",
                 }))
 
+    @responses.activate
     def test_release_needs_discharge_macaroon_refresh(self):
-        @urlmatch(path=r".*/snap-release/$")
-        def snap_release_handler(url, request):
-            snap_release_handler.call_count += 1
-            if snap_release_handler.call_count == 1:
-                self.first_snap_release_request = request
-                return {
-                    "status_code": 401,
-                    "headers": {
-                        "WWW-Authenticate": "Macaroon needs_refresh=1"}}
-            else:
-                return self._snap_release_handler(url, request)
-        snap_release_handler.call_count = 0
-
         store_secrets = self._make_store_secrets()
-        with HTTMock(self._channels_handler):
-            snap = self.factory.makeSnap(
-                store_upload=True,
-                store_series=self.factory.makeSnappySeries(name="rolling"),
-                store_name="test-snap", store_secrets=store_secrets,
-                store_channels=["stable", "edge"])
+        snap = self.factory.makeSnap(
+            store_upload=True,
+            store_series=self.factory.makeSnappySeries(name="rolling"),
+            store_name="test-snap", store_secrets=store_secrets,
+            store_channels=["stable", "edge"])
         snapbuild = self.factory.makeSnapBuild(snap=snap)
-        with HTTMock(snap_release_handler, self._macaroon_refresh_handler):
-            self.client.release(snapbuild, 1)
-        self.assertEqual(2, snap_release_handler.call_count)
+        responses.add(
+            "POST", "http://sca.example/dev/api/snap-release/";, status=401,
+            headers={"WWW-Authenticate": "Macaroon needs_refresh=1"})
+        self._addMacaroonRefreshResponse()
+        self._addSnapReleaseResponse()
+        self.client.release(snapbuild, 1)
+        requests = [call.request for call in responses.calls]
+        self.assertThat(requests, MatchesListwise([
+            MatchesStructure.byEquality(path_url="/dev/api/snap-release/"),
+            MatchesStructure.byEquality(path_url="/api/v2/tokens/refresh"),
+            MatchesStructure.byEquality(path_url="/dev/api/snap-release/"),
+            ]))
         self.assertNotEqual(
             store_secrets["discharge"], snap.store_secrets["discharge"])
 
+    @responses.activate
     def test_release_error(self):
-        @urlmatch(path=r".*/snap-release/$")
-        def handler(url, request):
-            return {
-                "status_code": 503,
-                "content": {"error_list": [{"message": "Failed to publish"}]},
-                }
-
-        with HTTMock(self._channels_handler):
-            snap = self.factory.makeSnap(
-                store_upload=True,
-                store_series=self.factory.makeSnappySeries(name="rolling"),
-                store_name="test-snap",
-                store_secrets=self._make_store_secrets(),
-                store_channels=["stable", "edge"])
+        snap = self.factory.makeSnap(
+            store_upload=True,
+            store_series=self.factory.makeSnappySeries(name="rolling"),
+            store_name="test-snap", store_secrets=self._make_store_secrets(),
+            store_channels=["stable", "edge"])
         snapbuild = self.factory.makeSnapBuild(snap=snap)
-        with HTTMock(handler):
-            self.assertRaisesWithContent(
-                ReleaseFailedResponse, "Failed to publish",
-                self.client.release, snapbuild, 1)
+        responses.add(
+            "POST", "http://sca.example/dev/api/snap-release/";, status=503,
+            json={"error_list": [{"message": "Failed to publish"}]})
+        self.assertRaisesWithContent(
+            ReleaseFailedResponse, "Failed to publish",
+            self.client.release, snapbuild, 1)
 
+    @responses.activate
     def test_release_404(self):
-        @urlmatch(path=r".*/snap-release/$")
-        def handler(url, request):
-            return {"status_code": 404, "reason": b"Not found"}
-
-        with HTTMock(self._channels_handler):
-            snap = self.factory.makeSnap(
-                store_upload=True,
-                store_series=self.factory.makeSnappySeries(name="rolling"),
-                store_name="test-snap",
-                store_secrets=self._make_store_secrets(),
-                store_channels=["stable", "edge"])
+        snap = self.factory.makeSnap(
+            store_upload=True,
+            store_series=self.factory.makeSnappySeries(name="rolling"),
+            store_name="test-snap", store_secrets=self._make_store_secrets(),
+            store_channels=["stable", "edge"])
         snapbuild = self.factory.makeSnapBuild(snap=snap)
-        with HTTMock(handler):
-            self.assertRaisesWithContent(
-                ReleaseFailedResponse, b"404 Client Error: Not found",
-                self.client.release, snapbuild, 1)
+        responses.add(
+            "POST", "http://sca.example/dev/api/snap-release/";, status=404)
+        self.assertRaisesWithContent(
+            ReleaseFailedResponse, b"404 Client Error: Not Found",
+            self.client.release, snapbuild, 1)

=== modified file 'setup.py'
--- setup.py	2018-05-21 20:30:16 +0000
+++ setup.py	2018-05-31 10:48:11 +0000
@@ -160,7 +160,6 @@
         'feedvalidator',
         'fixtures',
         'html5browser',
-        'httmock',
         'importlib-resources',
         'ipython',
         'jsautobuild',
@@ -209,6 +208,7 @@
         'rabbitfixture',
         'requests',
         'requests-toolbelt',
+        'responses',
         'scandir',
         'setproctitle',
         'setuptools',


Follow ups