Repository: lundberg/respx

Issue:
Optional assertion that all added patterns was triggered
To be in line with `responses` api, maybe name it the same `assert_all_requests_are_fired`.

Also decide what default for contextmanager use should be, `True` or `False`.

**Example:**
```py
with responsex.HTTPXMock(assert_all_requests_are_fired=False) as m:
    ...
```

Discussion:

Patch:
diff --git a/Makefile b/Makefile
index bb8ef75..abdffbc 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,7 @@
+.PHONY: all
+all: format lint mypy coverage
+
+
 .PHONY: test
 test:
 	python setup.py test $(test)
diff --git a/respx/__init__.py b/respx/__init__.py
index df7b555..c084937 100644
--- a/respx/__init__.py
+++ b/respx/__init__.py
@@ -3,7 +3,7 @@ from .mock import HTTPXMock
 __all__ = ["HTTPXMock"]
 
 # Expose mock api
-__httpx_mock = HTTPXMock()
+__httpx_mock = HTTPXMock(assert_all_called=False)
 __api_methods = list(filter(lambda m: not m.startswith("_"), dir(__httpx_mock)))
 __all__.extend(__api_methods)
 globals().update({method: getattr(__httpx_mock, method) for method in __api_methods})
diff --git a/respx/mock.py b/respx/mock.py
index b58b6f2..91d48c0 100644
--- a/respx/mock.py
+++ b/respx/mock.py
@@ -22,11 +22,14 @@ _get_response = BaseClient._get_response  # Pass-through reference
 
 
 class HTTPXMock:
-    def __init__(self):
-        self._patchers = []
-        self._patterns = []
-        self.aliases = {}
-        self.calls = []
+    def __init__(self, assert_all_called: bool = True) -> None:
+        self._assert_all_called = assert_all_called
+        self._patchers: typing.List[asynctest.mock._patch] = []
+        self._patterns: typing.List[RequestPattern] = []
+        self.aliases: typing.Dict[str, RequestPattern] = {}
+        self.calls: typing.List[
+            typing.Tuple[AsyncRequest, typing.Optional[AsyncResponse]]
+        ] = []
 
     def clear(self):
         self._patchers.clear()
@@ -40,6 +43,9 @@ class HTTPXMock:
 
     def __exit__(self, *args: typing.Any) -> None:
         self.stop()
+        if self._assert_all_called:
+            self.assert_all_called()
+        self.clear()
 
     def activate(self, func=None):
         """
@@ -81,7 +87,11 @@ class HTTPXMock:
         while self._patchers:
             patcher = self._patchers.pop()
             patcher.stop()
-        self.clear()
+
+    def assert_all_called(self):
+        assert all(
+            (pattern.called for pattern in self._patterns)
+        ), "not all requests called"
 
     def add(self, pattern: RequestPattern, alias: typing.Optional[str] = None) -> None:
         self._patterns.append(pattern)


Test patch:
diff --git a/tests/test_mock.py b/tests/test_mock.py
index ef34a4c..25250cf 100644
--- a/tests/test_mock.py
+++ b/tests/test_mock.py
@@ -19,15 +19,17 @@ class HTTPXMockTestCase(asynctest.TestCase):
 
         respx.start()
         response = httpx.get(url)
+        self.assertTrue(request.called)
+        self.assertEqual(response.status_code, 202)
+        self.assertEqual(response.text, "")
         self.assertEqual(len(respx.calls), 1)
+
         respx.stop()
+        self.assertEqual(len(respx.calls), 1)
 
+        respx.clear()
         self.assertEqual(len(respx.calls), 0)
 
-        self.assertTrue(request.called)
-        self.assertEqual(response.status_code, 202)
-        self.assertEqual(response.text, "")
-
     @respx.activate
     def test_activate_decorator(self):
         respx.get("https://foo/bar/", status_code=202)
@@ -96,17 +98,17 @@ class HTTPXMockTestCase(asynctest.TestCase):
         self.assertEqual(response.text, "whatever")
 
     def test_invalid_url_pattern(self):
-        with respx.HTTPXMock() as httpx_mock:
-            foobar = httpx_mock.get(["invalid"], content="whatever")
+        with respx.activate():
+            foobar = respx.get(["invalid"], content="whatever")
             with self.assertRaises(ValueError):
                 httpx.get("https://foo/bar/")
 
         self.assertFalse(foobar.called)
 
     def test_unknown_url(self):
-        with respx.HTTPXMock() as httpx_mock:
+        with respx.activate():
             url = "https://foo/bar/"
-            foobar = httpx_mock.post(url)  # Non-matching method
+            foobar = respx.post(url)  # Non-matching method
             response = httpx.get(url)
 
             self.assertFalse(foobar.called)
@@ -116,8 +118,8 @@ class HTTPXMockTestCase(asynctest.TestCase):
             )
             self.assertEqual(response.text, "")
 
-            self.assertEqual(len(httpx_mock.calls), 1)
-            request, response = httpx_mock.calls[-1]
+            self.assertEqual(len(respx.calls), 1)
+            request, response = respx.calls[-1]
             self.assertIsNotNone(request)
             self.assertIsNotNone(response)
 
@@ -278,11 +280,12 @@ class HTTPXMockTestCase(asynctest.TestCase):
         self.assertEqual(response.text, "foobar")
 
     def test_alias(self):
-        with respx.HTTPXMock() as httpx_mock:
+        with respx.activate() as m:
             url = "https://foo/bar/"
-            foobar = httpx_mock.get(url, alias="foobar")
-            self.assertIn("foobar", httpx_mock.aliases)
-            self.assertEqual(httpx_mock.aliases["foobar"].url, foobar.url)
+            foobar = respx.get(url, alias="foobar")
+            self.assertIn("foobar", respx.aliases)
+            self.assertEqual(respx.aliases["foobar"].url, foobar.url)
+            self.assertEqual(m["foobar"].url, foobar.url)
 
     def test_exception(self):
         with respx.HTTPXMock() as httpx_mock:
@@ -317,18 +320,53 @@ class HTTPXMockTestCase(asynctest.TestCase):
             )
             self.assertEqual(response.text, "foobar #123")
 
+    def test_assert_all_called_fail(self):
+        with self.assertRaises(AssertionError):
+            with respx.HTTPXMock() as httpx_mock:
+                request1 = httpx_mock.get("https://foo/bar/1/", status_code=404)
+                request2 = httpx_mock.post("https://foo/bar/", status_code=201)
+
+                response = httpx.get("https://foo/bar/1/")
+
+                self.assertEqual(response.status_code, 404)
+                self.assertTrue(request1.called)
+                self.assertFalse(request2.called)
+
+    def test_assert_all_called_disabled(self):
+        with respx.HTTPXMock(assert_all_called=False) as httpx_mock:
+            request1 = httpx_mock.get("https://foo/bar/1/", status_code=404)
+            request2 = httpx_mock.post("https://foo/bar/", status_code=201)
+
+            response = httpx.get("https://foo/bar/1/")
+
+            self.assertEqual(response.status_code, 404)
+            self.assertTrue(request1.called)
+            self.assertFalse(request2.called)
+
+    def test_assert_all_called_sucess(self):
+        with respx.HTTPXMock(assert_all_called=True) as httpx_mock:
+            request1 = httpx_mock.get("https://foo/bar/1/", status_code=404)
+            request2 = httpx_mock.post("https://foo/bar/", status_code=201)
+
+            response = httpx.get("https://foo/bar/1/")
+            response = httpx.post("https://foo/bar/")
+
+            self.assertEqual(response.status_code, 201)
+            self.assertTrue(request1.called)
+            self.assertTrue(request2.called)
+
     async def test_stats(self, backend=None):
-        with respx.HTTPXMock() as httpx_mock:
+        with respx.activate():
             url = "https://foo/bar/1/"
-            httpx_mock.get(re.compile("http://some/url"))
-            httpx_mock.delete("http://some/url")
+            respx.get(re.compile("http://some/url"))
+            respx.delete("http://some/url")
 
-            foobar1 = httpx_mock.get(url, status_code=202, alias="get_foobar")
-            foobar2 = httpx_mock.delete(url, status_code=200, alias="del_foobar")
+            foobar1 = respx.get(url, status_code=202, alias="get_foobar")
+            foobar2 = respx.delete(url, status_code=200, alias="del_foobar")
 
             self.assertFalse(foobar1.called)
             self.assertEqual(len(foobar1.calls), 0)
-            self.assertEqual(len(httpx_mock.calls), 0)
+            self.assertEqual(len(respx.calls), 0)
 
             async with httpx.AsyncClient(backend=backend) as client:
                 get_response = await client.get(url)
@@ -355,15 +393,15 @@ class HTTPXMockTestCase(asynctest.TestCase):
             self.assertEqual(_response.status_code, 200)
             self.assertEqual(_response.status_code, del_response.status_code)
 
-            self.assertEqual(len(httpx_mock.calls), 2)
-            self.assertEqual(httpx_mock.calls[0], foobar1.calls[-1])
-            self.assertEqual(httpx_mock.calls[1], foobar2.calls[-1])
+            self.assertEqual(len(respx.calls), 2)
+            self.assertEqual(respx.calls[0], foobar1.calls[-1])
+            self.assertEqual(respx.calls[1], foobar2.calls[-1])
 
-            alias = httpx_mock.aliases["get_foobar"]
+            alias = respx.aliases["get_foobar"]
             self.assertEqual(alias, foobar1)
             self.assertEqual(alias.alias, foobar1.alias)
 
-            alias = httpx_mock["del_foobar"]
+            alias = respx.aliases["del_foobar"]
             self.assertEqual(alias, foobar2)
             self.assertEqual(alias.alias, foobar2.alias)