浏览代码

Fixed #14611 -- Added query_params argument to RequestFactory and Client classes.

Tom Carrick 1 年之前
父节点
当前提交
a03593967f

+ 233 - 49
django/test/client.py

@@ -381,13 +381,22 @@ class RequestFactory:
     just as if that view had been hooked up using a URLconf.
     just as if that view had been hooked up using a URLconf.
     """
     """
 
 
-    def __init__(self, *, json_encoder=DjangoJSONEncoder, headers=None, **defaults):
+    def __init__(
+        self,
+        *,
+        json_encoder=DjangoJSONEncoder,
+        headers=None,
+        query_params=None,
+        **defaults,
+    ):
         self.json_encoder = json_encoder
         self.json_encoder = json_encoder
         self.defaults = defaults
         self.defaults = defaults
         self.cookies = SimpleCookie()
         self.cookies = SimpleCookie()
         self.errors = BytesIO()
         self.errors = BytesIO()
         if headers:
         if headers:
             self.defaults.update(HttpHeaders.to_wsgi_names(headers))
             self.defaults.update(HttpHeaders.to_wsgi_names(headers))
+        if query_params:
+            self.defaults["QUERY_STRING"] = urlencode(query_params, doseq=True)
 
 
     def _base_environ(self, **request):
     def _base_environ(self, **request):
         """
         """
@@ -459,18 +468,21 @@ class RequestFactory:
         # Refs comment in `get_bytes_from_wsgi()`.
         # Refs comment in `get_bytes_from_wsgi()`.
         return path.decode("iso-8859-1")
         return path.decode("iso-8859-1")
 
 
-    def get(self, path, data=None, secure=False, *, headers=None, **extra):
+    def get(
+        self, path, data=None, secure=False, *, headers=None, query_params=None, **extra
+    ):
         """Construct a GET request."""
         """Construct a GET request."""
-        data = {} if data is None else data
+        if query_params and data:
+            raise ValueError("query_params and data arguments are mutually exclusive.")
+        query_params = data or query_params
+        query_params = {} if query_params is None else query_params
         return self.generic(
         return self.generic(
             "GET",
             "GET",
             path,
             path,
             secure=secure,
             secure=secure,
             headers=headers,
             headers=headers,
-            **{
+            query_params=query_params,
-                "QUERY_STRING": urlencode(data, doseq=True),
+            **extra,
-                **extra,
-            },
         )
         )
 
 
     def post(
     def post(
@@ -481,6 +493,7 @@ class RequestFactory:
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Construct a POST request."""
         """Construct a POST request."""
@@ -494,26 +507,37 @@ class RequestFactory:
             content_type,
             content_type,
             secure=secure,
             secure=secure,
             headers=headers,
             headers=headers,
+            query_params=query_params,
             **extra,
             **extra,
         )
         )
 
 
-    def head(self, path, data=None, secure=False, *, headers=None, **extra):
+    def head(
+        self, path, data=None, secure=False, *, headers=None, query_params=None, **extra
+    ):
         """Construct a HEAD request."""
         """Construct a HEAD request."""
-        data = {} if data is None else data
+        if query_params and data:
+            raise ValueError("query_params and data arguments are mutually exclusive.")
+        query_params = data or query_params
+        query_params = {} if query_params is None else query_params
         return self.generic(
         return self.generic(
             "HEAD",
             "HEAD",
             path,
             path,
             secure=secure,
             secure=secure,
             headers=headers,
             headers=headers,
-            **{
+            query_params=query_params,
-                "QUERY_STRING": urlencode(data, doseq=True),
+            **extra,
-                **extra,
-            },
         )
         )
 
 
-    def trace(self, path, secure=False, *, headers=None, **extra):
+    def trace(self, path, secure=False, *, headers=None, query_params=None, **extra):
         """Construct a TRACE request."""
         """Construct a TRACE request."""
-        return self.generic("TRACE", path, secure=secure, headers=headers, **extra)
+        return self.generic(
+            "TRACE",
+            path,
+            secure=secure,
+            headers=headers,
+            query_params=query_params,
+            **extra,
+        )
 
 
     def options(
     def options(
         self,
         self,
@@ -523,11 +547,19 @@ class RequestFactory:
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         "Construct an OPTIONS request."
         "Construct an OPTIONS request."
         return self.generic(
         return self.generic(
-            "OPTIONS", path, data, content_type, secure=secure, headers=headers, **extra
+            "OPTIONS",
+            path,
+            data,
+            content_type,
+            secure=secure,
+            headers=headers,
+            query_params=query_params,
+            **extra,
         )
         )
 
 
     def put(
     def put(
@@ -538,12 +570,20 @@ class RequestFactory:
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Construct a PUT request."""
         """Construct a PUT request."""
         data = self._encode_json(data, content_type)
         data = self._encode_json(data, content_type)
         return self.generic(
         return self.generic(
-            "PUT", path, data, content_type, secure=secure, headers=headers, **extra
+            "PUT",
+            path,
+            data,
+            content_type,
+            secure=secure,
+            headers=headers,
+            query_params=query_params,
+            **extra,
         )
         )
 
 
     def patch(
     def patch(
@@ -554,12 +594,20 @@ class RequestFactory:
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Construct a PATCH request."""
         """Construct a PATCH request."""
         data = self._encode_json(data, content_type)
         data = self._encode_json(data, content_type)
         return self.generic(
         return self.generic(
-            "PATCH", path, data, content_type, secure=secure, headers=headers, **extra
+            "PATCH",
+            path,
+            data,
+            content_type,
+            secure=secure,
+            headers=headers,
+            query_params=query_params,
+            **extra,
         )
         )
 
 
     def delete(
     def delete(
@@ -570,12 +618,20 @@ class RequestFactory:
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Construct a DELETE request."""
         """Construct a DELETE request."""
         data = self._encode_json(data, content_type)
         data = self._encode_json(data, content_type)
         return self.generic(
         return self.generic(
-            "DELETE", path, data, content_type, secure=secure, headers=headers, **extra
+            "DELETE",
+            path,
+            data,
+            content_type,
+            secure=secure,
+            headers=headers,
+            query_params=query_params,
+            **extra,
         )
         )
 
 
     def generic(
     def generic(
@@ -587,6 +643,7 @@ class RequestFactory:
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Construct an arbitrary HTTP request."""
         """Construct an arbitrary HTTP request."""
@@ -608,6 +665,8 @@ class RequestFactory:
             )
             )
         if headers:
         if headers:
             extra.update(HttpHeaders.to_wsgi_names(headers))
             extra.update(HttpHeaders.to_wsgi_names(headers))
+        if query_params:
+            extra["QUERY_STRING"] = urlencode(query_params, doseq=True)
         r.update(extra)
         r.update(extra)
         # If QUERY_STRING is absent or empty, we want to extract it from the URL.
         # If QUERY_STRING is absent or empty, we want to extract it from the URL.
         if not r.get("QUERY_STRING"):
         if not r.get("QUERY_STRING"):
@@ -685,6 +744,7 @@ class AsyncRequestFactory(RequestFactory):
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Construct an arbitrary HTTP request."""
         """Construct an arbitrary HTTP request."""
@@ -705,18 +765,20 @@ class AsyncRequestFactory(RequestFactory):
                 ]
                 ]
             )
             )
             s["_body_file"] = FakePayload(data)
             s["_body_file"] = FakePayload(data)
-        if query_string := extra.pop("QUERY_STRING", None):
+        if query_params:
+            s["query_string"] = urlencode(query_params, doseq=True)
+        elif query_string := extra.pop("QUERY_STRING", None):
             s["query_string"] = query_string
             s["query_string"] = query_string
+        else:
+            # If QUERY_STRING is absent or empty, we want to extract it from
+            # the URL.
+            s["query_string"] = parsed[4]
         if headers:
         if headers:
             extra.update(HttpHeaders.to_asgi_names(headers))
             extra.update(HttpHeaders.to_asgi_names(headers))
         s["headers"] += [
         s["headers"] += [
             (key.lower().encode("ascii"), value.encode("latin1"))
             (key.lower().encode("ascii"), value.encode("latin1"))
             for key, value in extra.items()
             for key, value in extra.items()
         ]
         ]
-        # If QUERY_STRING is absent or empty, we want to extract it from the
-        # URL.
-        if not s.get("query_string"):
-            s["query_string"] = parsed[4]
         return self.request(**s)
         return self.request(**s)
 
 
 
 
@@ -889,7 +951,14 @@ class ClientMixin:
         return response._json
         return response._json
 
 
     def _follow_redirect(
     def _follow_redirect(
-        self, response, *, data="", content_type="", headers=None, **extra
+        self,
+        response,
+        *,
+        data="",
+        content_type="",
+        headers=None,
+        query_params=None,
+        **extra,
     ):
     ):
         """Follow a single redirect contained in response using GET."""
         """Follow a single redirect contained in response using GET."""
         response_url = response.url
         response_url = response.url
@@ -934,6 +1003,7 @@ class ClientMixin:
             content_type=content_type,
             content_type=content_type,
             follow=False,
             follow=False,
             headers=headers,
             headers=headers,
+            query_params=query_params,
             **extra,
             **extra,
         )
         )
 
 
@@ -978,9 +1048,10 @@ class Client(ClientMixin, RequestFactory):
         raise_request_exception=True,
         raise_request_exception=True,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **defaults,
         **defaults,
     ):
     ):
-        super().__init__(headers=headers, **defaults)
+        super().__init__(headers=headers, query_params=query_params, **defaults)
         self.handler = ClientHandler(enforce_csrf_checks)
         self.handler = ClientHandler(enforce_csrf_checks)
         self.raise_request_exception = raise_request_exception
         self.raise_request_exception = raise_request_exception
         self.exc_info = None
         self.exc_info = None
@@ -1042,15 +1113,23 @@ class Client(ClientMixin, RequestFactory):
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Request a response from the server using GET."""
         """Request a response from the server using GET."""
         self.extra = extra
         self.extra = extra
         self.headers = headers
         self.headers = headers
-        response = super().get(path, data=data, secure=secure, headers=headers, **extra)
+        response = super().get(
+            path,
+            data=data,
+            secure=secure,
+            headers=headers,
+            query_params=query_params,
+            **extra,
+        )
         if follow:
         if follow:
             response = self._handle_redirects(
             response = self._handle_redirects(
-                response, data=data, headers=headers, **extra
+                response, data=data, headers=headers, query_params=query_params, **extra
             )
             )
         return response
         return response
 
 
@@ -1063,6 +1142,7 @@ class Client(ClientMixin, RequestFactory):
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Request a response from the server using POST."""
         """Request a response from the server using POST."""
@@ -1074,11 +1154,17 @@ class Client(ClientMixin, RequestFactory):
             content_type=content_type,
             content_type=content_type,
             secure=secure,
             secure=secure,
             headers=headers,
             headers=headers,
+            query_params=query_params,
             **extra,
             **extra,
         )
         )
         if follow:
         if follow:
             response = self._handle_redirects(
             response = self._handle_redirects(
-                response, data=data, content_type=content_type, headers=headers, **extra
+                response,
+                data=data,
+                content_type=content_type,
+                headers=headers,
+                query_params=query_params,
+                **extra,
             )
             )
         return response
         return response
 
 
@@ -1090,17 +1176,23 @@ class Client(ClientMixin, RequestFactory):
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Request a response from the server using HEAD."""
         """Request a response from the server using HEAD."""
         self.extra = extra
         self.extra = extra
         self.headers = headers
         self.headers = headers
         response = super().head(
         response = super().head(
-            path, data=data, secure=secure, headers=headers, **extra
+            path,
+            data=data,
+            secure=secure,
+            headers=headers,
+            query_params=query_params,
+            **extra,
         )
         )
         if follow:
         if follow:
             response = self._handle_redirects(
             response = self._handle_redirects(
-                response, data=data, headers=headers, **extra
+                response, data=data, headers=headers, query_params=query_params, **extra
             )
             )
         return response
         return response
 
 
@@ -1113,6 +1205,7 @@ class Client(ClientMixin, RequestFactory):
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Request a response from the server using OPTIONS."""
         """Request a response from the server using OPTIONS."""
@@ -1124,11 +1217,17 @@ class Client(ClientMixin, RequestFactory):
             content_type=content_type,
             content_type=content_type,
             secure=secure,
             secure=secure,
             headers=headers,
             headers=headers,
+            query_params=query_params,
             **extra,
             **extra,
         )
         )
         if follow:
         if follow:
             response = self._handle_redirects(
             response = self._handle_redirects(
-                response, data=data, content_type=content_type, headers=headers, **extra
+                response,
+                data=data,
+                content_type=content_type,
+                headers=headers,
+                query_params=query_params,
+                **extra,
             )
             )
         return response
         return response
 
 
@@ -1141,6 +1240,7 @@ class Client(ClientMixin, RequestFactory):
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Send a resource to the server using PUT."""
         """Send a resource to the server using PUT."""
@@ -1152,11 +1252,17 @@ class Client(ClientMixin, RequestFactory):
             content_type=content_type,
             content_type=content_type,
             secure=secure,
             secure=secure,
             headers=headers,
             headers=headers,
+            query_params=query_params,
             **extra,
             **extra,
         )
         )
         if follow:
         if follow:
             response = self._handle_redirects(
             response = self._handle_redirects(
-                response, data=data, content_type=content_type, headers=headers, **extra
+                response,
+                data=data,
+                content_type=content_type,
+                headers=headers,
+                query_params=query_params,
+                **extra,
             )
             )
         return response
         return response
 
 
@@ -1169,6 +1275,7 @@ class Client(ClientMixin, RequestFactory):
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Send a resource to the server using PATCH."""
         """Send a resource to the server using PATCH."""
@@ -1180,11 +1287,17 @@ class Client(ClientMixin, RequestFactory):
             content_type=content_type,
             content_type=content_type,
             secure=secure,
             secure=secure,
             headers=headers,
             headers=headers,
+            query_params=query_params,
             **extra,
             **extra,
         )
         )
         if follow:
         if follow:
             response = self._handle_redirects(
             response = self._handle_redirects(
-                response, data=data, content_type=content_type, headers=headers, **extra
+                response,
+                data=data,
+                content_type=content_type,
+                headers=headers,
+                query_params=query_params,
+                **extra,
             )
             )
         return response
         return response
 
 
@@ -1197,6 +1310,7 @@ class Client(ClientMixin, RequestFactory):
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Send a DELETE request to the server."""
         """Send a DELETE request to the server."""
@@ -1208,11 +1322,17 @@ class Client(ClientMixin, RequestFactory):
             content_type=content_type,
             content_type=content_type,
             secure=secure,
             secure=secure,
             headers=headers,
             headers=headers,
+            query_params=query_params,
             **extra,
             **extra,
         )
         )
         if follow:
         if follow:
             response = self._handle_redirects(
             response = self._handle_redirects(
-                response, data=data, content_type=content_type, headers=headers, **extra
+                response,
+                data=data,
+                content_type=content_type,
+                headers=headers,
+                query_params=query_params,
+                **extra,
             )
             )
         return response
         return response
 
 
@@ -1224,17 +1344,23 @@ class Client(ClientMixin, RequestFactory):
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Send a TRACE request to the server."""
         """Send a TRACE request to the server."""
         self.extra = extra
         self.extra = extra
         self.headers = headers
         self.headers = headers
         response = super().trace(
         response = super().trace(
-            path, data=data, secure=secure, headers=headers, **extra
+            path,
+            data=data,
+            secure=secure,
+            headers=headers,
+            query_params=query_params,
+            **extra,
         )
         )
         if follow:
         if follow:
             response = self._handle_redirects(
             response = self._handle_redirects(
-                response, data=data, headers=headers, **extra
+                response, data=data, headers=headers, query_params=query_params, **extra
             )
             )
         return response
         return response
 
 
@@ -1244,6 +1370,7 @@ class Client(ClientMixin, RequestFactory):
         data="",
         data="",
         content_type="",
         content_type="",
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """
         """
@@ -1257,6 +1384,7 @@ class Client(ClientMixin, RequestFactory):
                 data=data,
                 data=data,
                 content_type=content_type,
                 content_type=content_type,
                 headers=headers,
                 headers=headers,
+                query_params=query_params,
                 **extra,
                 **extra,
             )
             )
             response.redirect_chain = redirect_chain
             response.redirect_chain = redirect_chain
@@ -1278,9 +1406,10 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         raise_request_exception=True,
         raise_request_exception=True,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **defaults,
         **defaults,
     ):
     ):
-        super().__init__(headers=headers, **defaults)
+        super().__init__(headers=headers, query_params=query_params, **defaults)
         self.handler = AsyncClientHandler(enforce_csrf_checks)
         self.handler = AsyncClientHandler(enforce_csrf_checks)
         self.raise_request_exception = raise_request_exception
         self.raise_request_exception = raise_request_exception
         self.exc_info = None
         self.exc_info = None
@@ -1341,17 +1470,23 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Request a response from the server using GET."""
         """Request a response from the server using GET."""
         self.extra = extra
         self.extra = extra
         self.headers = headers
         self.headers = headers
         response = await super().get(
         response = await super().get(
-            path, data=data, secure=secure, headers=headers, **extra
+            path,
+            data=data,
+            secure=secure,
+            headers=headers,
+            query_params=query_params,
+            **extra,
         )
         )
         if follow:
         if follow:
             response = await self._ahandle_redirects(
             response = await self._ahandle_redirects(
-                response, data=data, headers=headers, **extra
+                response, data=data, headers=headers, query_params=query_params, **extra
             )
             )
         return response
         return response
 
 
@@ -1364,6 +1499,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Request a response from the server using POST."""
         """Request a response from the server using POST."""
@@ -1375,11 +1511,17 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
             content_type=content_type,
             content_type=content_type,
             secure=secure,
             secure=secure,
             headers=headers,
             headers=headers,
+            query_params=query_params,
             **extra,
             **extra,
         )
         )
         if follow:
         if follow:
             response = await self._ahandle_redirects(
             response = await self._ahandle_redirects(
-                response, data=data, content_type=content_type, headers=headers, **extra
+                response,
+                data=data,
+                content_type=content_type,
+                headers=headers,
+                query_params=query_params,
+                **extra,
             )
             )
         return response
         return response
 
 
@@ -1391,17 +1533,23 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Request a response from the server using HEAD."""
         """Request a response from the server using HEAD."""
         self.extra = extra
         self.extra = extra
         self.headers = headers
         self.headers = headers
         response = await super().head(
         response = await super().head(
-            path, data=data, secure=secure, headers=headers, **extra
+            path,
+            data=data,
+            secure=secure,
+            headers=headers,
+            query_params=query_params,
+            **extra,
         )
         )
         if follow:
         if follow:
             response = await self._ahandle_redirects(
             response = await self._ahandle_redirects(
-                response, data=data, headers=headers, **extra
+                response, data=data, headers=headers, query_params=query_params, **extra
             )
             )
         return response
         return response
 
 
@@ -1414,6 +1562,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Request a response from the server using OPTIONS."""
         """Request a response from the server using OPTIONS."""
@@ -1425,11 +1574,17 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
             content_type=content_type,
             content_type=content_type,
             secure=secure,
             secure=secure,
             headers=headers,
             headers=headers,
+            query_params=query_params,
             **extra,
             **extra,
         )
         )
         if follow:
         if follow:
             response = await self._ahandle_redirects(
             response = await self._ahandle_redirects(
-                response, data=data, content_type=content_type, headers=headers, **extra
+                response,
+                data=data,
+                content_type=content_type,
+                headers=headers,
+                query_params=query_params,
+                **extra,
             )
             )
         return response
         return response
 
 
@@ -1442,6 +1597,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Send a resource to the server using PUT."""
         """Send a resource to the server using PUT."""
@@ -1453,11 +1609,17 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
             content_type=content_type,
             content_type=content_type,
             secure=secure,
             secure=secure,
             headers=headers,
             headers=headers,
+            query_params=query_params,
             **extra,
             **extra,
         )
         )
         if follow:
         if follow:
             response = await self._ahandle_redirects(
             response = await self._ahandle_redirects(
-                response, data=data, content_type=content_type, headers=headers, **extra
+                response,
+                data=data,
+                content_type=content_type,
+                headers=headers,
+                query_params=query_params,
+                **extra,
             )
             )
         return response
         return response
 
 
@@ -1470,6 +1632,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Send a resource to the server using PATCH."""
         """Send a resource to the server using PATCH."""
@@ -1481,11 +1644,17 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
             content_type=content_type,
             content_type=content_type,
             secure=secure,
             secure=secure,
             headers=headers,
             headers=headers,
+            query_params=query_params,
             **extra,
             **extra,
         )
         )
         if follow:
         if follow:
             response = await self._ahandle_redirects(
             response = await self._ahandle_redirects(
-                response, data=data, content_type=content_type, headers=headers, **extra
+                response,
+                data=data,
+                content_type=content_type,
+                headers=headers,
+                query_params=query_params,
+                **extra,
             )
             )
         return response
         return response
 
 
@@ -1498,6 +1667,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Send a DELETE request to the server."""
         """Send a DELETE request to the server."""
@@ -1509,11 +1679,17 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
             content_type=content_type,
             content_type=content_type,
             secure=secure,
             secure=secure,
             headers=headers,
             headers=headers,
+            query_params=query_params,
             **extra,
             **extra,
         )
         )
         if follow:
         if follow:
             response = await self._ahandle_redirects(
             response = await self._ahandle_redirects(
-                response, data=data, content_type=content_type, headers=headers, **extra
+                response,
+                data=data,
+                content_type=content_type,
+                headers=headers,
+                query_params=query_params,
+                **extra,
             )
             )
         return response
         return response
 
 
@@ -1525,17 +1701,23 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         secure=False,
         secure=False,
         *,
         *,
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """Send a TRACE request to the server."""
         """Send a TRACE request to the server."""
         self.extra = extra
         self.extra = extra
         self.headers = headers
         self.headers = headers
         response = await super().trace(
         response = await super().trace(
-            path, data=data, secure=secure, headers=headers, **extra
+            path,
+            data=data,
+            secure=secure,
+            headers=headers,
+            query_params=query_params,
+            **extra,
         )
         )
         if follow:
         if follow:
             response = await self._ahandle_redirects(
             response = await self._ahandle_redirects(
-                response, data=data, headers=headers, **extra
+                response, data=data, headers=headers, query_params=query_params, **extra
             )
             )
         return response
         return response
 
 
@@ -1545,6 +1727,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         data="",
         data="",
         content_type="",
         content_type="",
         headers=None,
         headers=None,
+        query_params=None,
         **extra,
         **extra,
     ):
     ):
         """
         """
@@ -1558,6 +1741,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
                 data=data,
                 data=data,
                 content_type=content_type,
                 content_type=content_type,
                 headers=headers,
                 headers=headers,
+                query_params=query_params,
                 **extra,
                 **extra,
             )
             )
             response.redirect_chain = redirect_chain
             response.redirect_chain = redirect_chain

+ 11 - 0
docs/releases/5.1.txt

@@ -224,6 +224,17 @@ Tests
 * The Django test runner now supports a ``--screenshots`` option to save
 * The Django test runner now supports a ``--screenshots`` option to save
   screenshots for Selenium tests.
   screenshots for Selenium tests.
 
 
+* The :class:`~django.test.RequestFactory`,
+  :class:`~django.test.AsyncRequestFactory`, :class:`~django.test.Client`, and
+  :class:`~django.test.AsyncClient` classes now support the ``query_params``
+  parameter, which accepts a dictionary of query string keys and values. This
+  allows setting query strings on any HTTP methods more easily.
+
+  .. code-block:: python
+
+     self.client.post("/items/1", query_params={"action": "delete"})
+     await self.async_client.post("/items/1", query_params={"action": "delete"})
+
 URLs
 URLs
 ~~~~
 ~~~~
 
 

+ 8 - 0
docs/topics/testing/advanced.txt

@@ -32,6 +32,10 @@ restricted subset of the test client API:
   attributes must be supplied by the test itself if required
   attributes must be supplied by the test itself if required
   for the view to function properly.
   for the view to function properly.
 
 
+.. versionchanged:: 5.1
+
+    The ``query_params`` parameter was added.
+
 Example
 Example
 -------
 -------
 
 
@@ -85,6 +89,10 @@ difference being that it returns ``ASGIRequest`` instances rather than
 Arbitrary keyword arguments in ``defaults`` are added directly into the ASGI
 Arbitrary keyword arguments in ``defaults`` are added directly into the ASGI
 scope.
 scope.
 
 
+.. versionchanged:: 5.1
+
+    The ``query_params`` parameter was added.
+
 Testing class-based views
 Testing class-based views
 =========================
 =========================
 
 

+ 82 - 33
docs/topics/testing/tools.txt

@@ -120,7 +120,7 @@ Making requests
 
 
 Use the ``django.test.Client`` class to make requests.
 Use the ``django.test.Client`` class to make requests.
 
 
-.. class:: Client(enforce_csrf_checks=False, raise_request_exception=True, json_encoder=DjangoJSONEncoder, *, headers=None, **defaults)
+.. class:: Client(enforce_csrf_checks=False, raise_request_exception=True, json_encoder=DjangoJSONEncoder, *, headers=None, query_params=None, **defaults)
 
 
     A testing HTTP client. Takes several arguments that can customize behavior.
     A testing HTTP client. Takes several arguments that can customize behavior.
 
 
@@ -129,6 +129,9 @@ Use the ``django.test.Client`` class to make requests.
 
 
         client = Client(headers={"user-agent": "curl/7.79.1"})
         client = Client(headers={"user-agent": "curl/7.79.1"})
 
 
+    ``query_params`` allows you to specify the default query string that will
+    be set on every request.
+
     Arbitrary keyword arguments in ``**defaults`` set WSGI
     Arbitrary keyword arguments in ``**defaults`` set WSGI
     :pep:`environ variables <3333#environ-variables>`. For example, to set the
     :pep:`environ variables <3333#environ-variables>`. For example, to set the
     script name::
     script name::
@@ -140,8 +143,8 @@ Use the ``django.test.Client`` class to make requests.
         Keyword arguments starting with a ``HTTP_`` prefix are set as headers,
         Keyword arguments starting with a ``HTTP_`` prefix are set as headers,
         but the ``headers`` parameter should be preferred for readability.
         but the ``headers`` parameter should be preferred for readability.
 
 
-    The values from the ``headers`` and ``extra`` keyword arguments passed to
+    The values from the ``headers``, ``query_params``, and ``extra`` keyword
-    :meth:`~django.test.Client.get()`,
+    arguments passed to :meth:`~django.test.Client.get()`,
     :meth:`~django.test.Client.post()`, etc. have precedence over
     :meth:`~django.test.Client.post()`, etc. have precedence over
     the defaults passed to the class constructor.
     the defaults passed to the class constructor.
 
 
@@ -155,21 +158,25 @@ Use the ``django.test.Client`` class to make requests.
     The ``json_encoder`` argument allows setting a custom JSON encoder for
     The ``json_encoder`` argument allows setting a custom JSON encoder for
     the JSON serialization that's described in :meth:`post`.
     the JSON serialization that's described in :meth:`post`.
 
 
+    .. versionchanged:: 5.1
+
+        The ``query_params`` argument was added.
+
     Once you have a ``Client`` instance, you can call any of the following
     Once you have a ``Client`` instance, you can call any of the following
     methods:
     methods:
 
 
-    .. method:: Client.get(path, data=None, follow=False, secure=False, *, headers=None, **extra)
+    .. method:: Client.get(path, data=None, follow=False, secure=False, *, headers=None, query_params=None, **extra)
 
 
         Makes a GET request on the provided ``path`` and returns a ``Response``
         Makes a GET request on the provided ``path`` and returns a ``Response``
         object, which is documented below.
         object, which is documented below.
 
 
-        The key-value pairs in the ``data`` dictionary are used to create a GET
+        The key-value pairs in the ``query_params`` dictionary are used to set
-        data payload. For example:
+        query strings. For example:
 
 
         .. code-block:: pycon
         .. code-block:: pycon
 
 
             >>> c = Client()
             >>> c = Client()
-            >>> c.get("/customers/details/", {"name": "fred", "age": 7})
+            >>> c.get("/customers/details/", query_params={"name": "fred", "age": 7})
 
 
         ...will result in the evaluation of a GET request equivalent to:
         ...will result in the evaluation of a GET request equivalent to:
 
 
@@ -177,6 +184,10 @@ Use the ``django.test.Client`` class to make requests.
 
 
             /customers/details/?name=fred&age=7
             /customers/details/?name=fred&age=7
 
 
+        It is also possible to pass these parameters into the ``data``
+        parameter. However, ``query_params`` is preferred as it works for any
+        HTTP method.
+
         The ``headers`` parameter can be used to specify headers to be sent in
         The ``headers`` parameter can be used to specify headers to be sent in
         the request. For example:
         the request. For example:
 
 
@@ -185,7 +196,7 @@ Use the ``django.test.Client`` class to make requests.
             >>> c = Client()
             >>> c = Client()
             >>> c.get(
             >>> c.get(
             ...     "/customers/details/",
             ...     "/customers/details/",
-            ...     {"name": "fred", "age": 7},
+            ...     query_params={"name": "fred", "age": 7},
             ...     headers={"accept": "application/json"},
             ...     headers={"accept": "application/json"},
             ... )
             ... )
 
 
@@ -211,8 +222,8 @@ Use the ``django.test.Client`` class to make requests.
             >>> c = Client()
             >>> c = Client()
             >>> c.get("/customers/details/?name=fred&age=7")
             >>> c.get("/customers/details/?name=fred&age=7")
 
 
-        If you provide a URL with both an encoded GET data and a data argument,
+        If you provide a URL with both an encoded GET data and either a
-        the data argument will take precedence.
+        query_params or data argument these arguments will take precedence.
 
 
         If you set ``follow`` to ``True`` the client will follow any redirects
         If you set ``follow`` to ``True`` the client will follow any redirects
         and a ``redirect_chain`` attribute will be set in the response object
         and a ``redirect_chain`` attribute will be set in the response object
@@ -230,7 +241,11 @@ Use the ``django.test.Client`` class to make requests.
         If you set ``secure`` to ``True`` the client will emulate an HTTPS
         If you set ``secure`` to ``True`` the client will emulate an HTTPS
         request.
         request.
 
 
-    .. method:: Client.post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, *, headers=None, **extra)
+        .. versionchanged:: 5.1
+
+            The ``query_params`` argument was added.
+
+    .. method:: Client.post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, *, headers=None, query_params=None, **extra)
 
 
         Makes a POST request on the provided ``path`` and returns a
         Makes a POST request on the provided ``path`` and returns a
         ``Response`` object, which is documented below.
         ``Response`` object, which is documented below.
@@ -321,8 +336,8 @@ Use the ``django.test.Client`` class to make requests.
         such as an image, this means you will need to open the file in
         such as an image, this means you will need to open the file in
         ``rb`` (read binary) mode.
         ``rb`` (read binary) mode.
 
 
-        The ``headers`` and ``extra`` parameters acts the same as for
+        The ``headers``, ``query_params``, and ``extra`` parameters acts the
-        :meth:`Client.get`.
+        same as for :meth:`Client.get`.
 
 
         If the URL you request with a POST contains encoded parameters, these
         If the URL you request with a POST contains encoded parameters, these
         parameters will be made available in the request.GET data. For example,
         parameters will be made available in the request.GET data. For example,
@@ -330,7 +345,9 @@ Use the ``django.test.Client`` class to make requests.
 
 
         .. code-block:: pycon
         .. code-block:: pycon
 
 
-            >>> c.post("/login/?visitor=true", {"name": "fred", "passwd": "secret"})
+            >>> c.post(
+            ...     "/login/", {"name": "fred", "passwd": "secret"}, query_params={"visitor": "true"}
+            ... )
 
 
         ... the view handling this request could interrogate request.POST
         ... the view handling this request could interrogate request.POST
         to retrieve the username and password, and could interrogate request.GET
         to retrieve the username and password, and could interrogate request.GET
@@ -343,14 +360,22 @@ Use the ``django.test.Client`` class to make requests.
         If you set ``secure`` to ``True`` the client will emulate an HTTPS
         If you set ``secure`` to ``True`` the client will emulate an HTTPS
         request.
         request.
 
 
-    .. method:: Client.head(path, data=None, follow=False, secure=False, *, headers=None, **extra)
+        .. versionchanged:: 5.1
+
+            The ``query_params`` argument was added.
+
+    .. method:: Client.head(path, data=None, follow=False, secure=False, *, headers=None, query_params=None, **extra)
 
 
         Makes a HEAD request on the provided ``path`` and returns a
         Makes a HEAD request on the provided ``path`` and returns a
         ``Response`` object. This method works just like :meth:`Client.get`,
         ``Response`` object. This method works just like :meth:`Client.get`,
-        including the ``follow``, ``secure``, ``headers``, and ``extra``
+        including the ``follow``, ``secure``, ``headers``, ``query_params``,
-        parameters, except it does not return a message body.
+        and ``extra`` parameters, except it does not return a message body.
 
 
-    .. method:: Client.options(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra)
+        .. versionchanged:: 5.1
+
+            The ``query_params`` argument was added.
+
+    .. method:: Client.options(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)
 
 
         Makes an OPTIONS request on the provided ``path`` and returns a
         Makes an OPTIONS request on the provided ``path`` and returns a
         ``Response`` object. Useful for testing RESTful interfaces.
         ``Response`` object. Useful for testing RESTful interfaces.
@@ -358,10 +383,14 @@ Use the ``django.test.Client`` class to make requests.
         When ``data`` is provided, it is used as the request body, and
         When ``data`` is provided, it is used as the request body, and
         a ``Content-Type`` header is set to ``content_type``.
         a ``Content-Type`` header is set to ``content_type``.
 
 
-        The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
+        The ``follow``, ``secure``, ``headers``, ``query_params``, and
-        the same as for :meth:`Client.get`.
+        ``extra`` parameters act the same as for :meth:`Client.get`.
+
+        .. versionchanged:: 5.1
 
 
-    .. method:: Client.put(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra)
+            The ``query_params`` argument was added.
+
+    .. method:: Client.put(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)
 
 
         Makes a PUT request on the provided ``path`` and returns a
         Makes a PUT request on the provided ``path`` and returns a
         ``Response`` object. Useful for testing RESTful interfaces.
         ``Response`` object. Useful for testing RESTful interfaces.
@@ -369,18 +398,26 @@ Use the ``django.test.Client`` class to make requests.
         When ``data`` is provided, it is used as the request body, and
         When ``data`` is provided, it is used as the request body, and
         a ``Content-Type`` header is set to ``content_type``.
         a ``Content-Type`` header is set to ``content_type``.
 
 
-        The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
+        The ``follow``, ``secure``, ``headers``, ``query_params``, and
-        the same as for :meth:`Client.get`.
+        ``extra`` parameters act the same as for :meth:`Client.get`.
+
+        .. versionchanged:: 5.1
+
+            The ``query_params`` argument was added.
 
 
-    .. method:: Client.patch(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra)
+    .. method:: Client.patch(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)
 
 
         Makes a PATCH request on the provided ``path`` and returns a
         Makes a PATCH request on the provided ``path`` and returns a
         ``Response`` object. Useful for testing RESTful interfaces.
         ``Response`` object. Useful for testing RESTful interfaces.
 
 
-        The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
+        The ``follow``, ``secure``, ``headers``, ``query_params``, and
-        the same as for :meth:`Client.get`.
+        ``extra`` parameters act the same as for :meth:`Client.get`.
 
 
-    .. method:: Client.delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, **extra)
+        .. versionchanged:: 5.1
+
+            The ``query_params`` argument was added.
+
+    .. method:: Client.delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)
 
 
         Makes a DELETE request on the provided ``path`` and returns a
         Makes a DELETE request on the provided ``path`` and returns a
         ``Response`` object. Useful for testing RESTful interfaces.
         ``Response`` object. Useful for testing RESTful interfaces.
@@ -388,10 +425,14 @@ Use the ``django.test.Client`` class to make requests.
         When ``data`` is provided, it is used as the request body, and
         When ``data`` is provided, it is used as the request body, and
         a ``Content-Type`` header is set to ``content_type``.
         a ``Content-Type`` header is set to ``content_type``.
 
 
-        The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
+        The ``follow``, ``secure``, ``headers``, ``query_params``, and
-        the same as for :meth:`Client.get`.
+        ``extra`` parameters act the same as for :meth:`Client.get`.
+
+        .. versionchanged:: 5.1
 
 
-    .. method:: Client.trace(path, follow=False, secure=False, *, headers=None, **extra)
+            The ``query_params`` argument was added.
+
+    .. method:: Client.trace(path, follow=False, secure=False, *, headers=None, query_params=None, **extra)
 
 
         Makes a TRACE request on the provided ``path`` and returns a
         Makes a TRACE request on the provided ``path`` and returns a
         ``Response`` object. Useful for simulating diagnostic probes.
         ``Response`` object. Useful for simulating diagnostic probes.
@@ -400,8 +441,12 @@ Use the ``django.test.Client`` class to make requests.
         parameter in order to comply with :rfc:`9110#section-9.3.8`, which
         parameter in order to comply with :rfc:`9110#section-9.3.8`, which
         mandates that TRACE requests must not have a body.
         mandates that TRACE requests must not have a body.
 
 
-        The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
+        The ``follow``, ``secure``, ``headers``, ``query_params``, and
-        the same as for :meth:`Client.get`.
+        ``extra`` parameters act the same as for :meth:`Client.get`.
+
+        .. versionchanged:: 5.1
+
+            The ``query_params`` argument was added.
 
 
     .. method:: Client.login(**credentials)
     .. method:: Client.login(**credentials)
     .. method:: Client.alogin(**credentials)
     .. method:: Client.alogin(**credentials)
@@ -1997,7 +2042,7 @@ If you are testing from an asynchronous function, you must also use the
 asynchronous test client. This is available as ``django.test.AsyncClient``,
 asynchronous test client. This is available as ``django.test.AsyncClient``,
 or as ``self.async_client`` on any test.
 or as ``self.async_client`` on any test.
 
 
-.. class:: AsyncClient(enforce_csrf_checks=False, raise_request_exception=True, *, headers=None, **defaults)
+.. class:: AsyncClient(enforce_csrf_checks=False, raise_request_exception=True, *, headers=None, query_params=None, **defaults)
 
 
 ``AsyncClient`` has the same methods and signatures as the synchronous (normal)
 ``AsyncClient`` has the same methods and signatures as the synchronous (normal)
 test client, with the following exceptions:
 test client, with the following exceptions:
@@ -2017,6 +2062,10 @@ test client, with the following exceptions:
 
 
     Support for the ``follow`` parameter was added to the ``AsyncClient``.
     Support for the ``follow`` parameter was added to the ``AsyncClient``.
 
 
+.. versionchanged:: 5.1
+
+    The ``query_params`` argument was added.
+
 Using ``AsyncClient`` any method that makes a request must be awaited::
 Using ``AsyncClient`` any method that makes a request must be awaited::
 
 
     async def test_my_thing(self):
     async def test_my_thing(self):

+ 96 - 0
tests/test_client/tests.py

@@ -1002,6 +1002,36 @@ class ClientTest(TestCase):
             )
             )
         self.assertEqual(response.content, b"named_temp_file")
         self.assertEqual(response.content, b"named_temp_file")
 
 
+    def test_query_params(self):
+        tests = (
+            "get",
+            "post",
+            "put",
+            "patch",
+            "delete",
+            "head",
+            "options",
+            "trace",
+        )
+        for method in tests:
+            with self.subTest(method=method):
+                client_method = getattr(self.client, method)
+                response = client_method("/get_view/", query_params={"example": "data"})
+                self.assertEqual(response.wsgi_request.GET["example"], "data")
+
+    def test_cannot_use_data_and_query_params_together(self):
+        tests = ["get", "head"]
+        msg = "query_params and data arguments are mutually exclusive."
+        for method in tests:
+            with self.subTest(method=method):
+                client_method = getattr(self.client, method)
+                with self.assertRaisesMessage(ValueError, msg):
+                    client_method(
+                        "/get_view/",
+                        data={"example": "data"},
+                        query_params={"q": "terms"},
+                    )
+
 
 
 @override_settings(
 @override_settings(
     MIDDLEWARE=["django.middleware.csrf.CsrfViewMiddleware"],
     MIDDLEWARE=["django.middleware.csrf.CsrfViewMiddleware"],
@@ -1127,6 +1157,23 @@ class RequestFactoryTest(SimpleTestCase):
             self.assertEqual(request.headers["x-another-header"], "some other value")
             self.assertEqual(request.headers["x-another-header"], "some other value")
             self.assertIn("HTTP_X_ANOTHER_HEADER", request.META)
             self.assertIn("HTTP_X_ANOTHER_HEADER", request.META)
 
 
+    def test_request_factory_query_params(self):
+        tests = (
+            "get",
+            "post",
+            "put",
+            "patch",
+            "delete",
+            "head",
+            "options",
+            "trace",
+        )
+        for method in tests:
+            with self.subTest(method=method):
+                factory = getattr(self.request_factory, method)
+                request = factory("/somewhere", query_params={"example": "data"})
+                self.assertEqual(request.GET["example"], "data")
+
 
 
 @override_settings(ROOT_URLCONF="test_client.urls")
 @override_settings(ROOT_URLCONF="test_client.urls")
 class AsyncClientTest(TestCase):
 class AsyncClientTest(TestCase):
@@ -1183,6 +1230,25 @@ class AsyncClientTest(TestCase):
         response = await self.async_client.get("/post_view/")
         response = await self.async_client.get("/post_view/")
         self.assertContains(response, "Viewing GET page.")
         self.assertContains(response, "Viewing GET page.")
 
 
+    async def test_query_params(self):
+        tests = (
+            "get",
+            "post",
+            "put",
+            "patch",
+            "delete",
+            "head",
+            "options",
+            "trace",
+        )
+        for method in tests:
+            with self.subTest(method=method):
+                client_method = getattr(self.async_client, method)
+                response = await client_method(
+                    "/async_get_view/", query_params={"example": "data"}
+                )
+                self.assertEqual(response.asgi_request.GET["example"], "data")
+
 
 
 @override_settings(ROOT_URLCONF="test_client.urls")
 @override_settings(ROOT_URLCONF="test_client.urls")
 class AsyncRequestFactoryTest(SimpleTestCase):
 class AsyncRequestFactoryTest(SimpleTestCase):
@@ -1264,3 +1330,33 @@ class AsyncRequestFactoryTest(SimpleTestCase):
         request = self.request_factory.get("/somewhere/", {"example": "data"})
         request = self.request_factory.get("/somewhere/", {"example": "data"})
         self.assertNotIn("Query-String", request.headers)
         self.assertNotIn("Query-String", request.headers)
         self.assertEqual(request.GET["example"], "data")
         self.assertEqual(request.GET["example"], "data")
+
+    def test_request_factory_query_params(self):
+        tests = (
+            "get",
+            "post",
+            "put",
+            "patch",
+            "delete",
+            "head",
+            "options",
+            "trace",
+        )
+        for method in tests:
+            with self.subTest(method=method):
+                factory = getattr(self.request_factory, method)
+                request = factory("/somewhere", query_params={"example": "data"})
+                self.assertEqual(request.GET["example"], "data")
+
+    def test_cannot_use_data_and_query_params_together(self):
+        tests = ["get", "head"]
+        msg = "query_params and data arguments are mutually exclusive."
+        for method in tests:
+            with self.subTest(method=method):
+                factory = getattr(self.request_factory, method)
+                with self.assertRaisesMessage(ValueError, msg):
+                    factory(
+                        "/somewhere",
+                        data={"example": "data"},
+                        query_params={"q": "terms"},
+                    )

+ 4 - 0
tests/test_client_regress/tests.py

@@ -1197,6 +1197,10 @@ class QueryStringTests(SimpleTestCase):
         self.assertEqual(response.context["get-foo"], "whiz")
         self.assertEqual(response.context["get-foo"], "whiz")
         self.assertIsNone(response.context["post-foo"])
         self.assertIsNone(response.context["post-foo"])
 
 
+        response = self.client.post("/request_data/", query_params={"foo": "whiz"})
+        self.assertEqual(response.context["get-foo"], "whiz")
+        self.assertIsNone(response.context["post-foo"])
+
         # POST data provided in the URL augments actual form data
         # POST data provided in the URL augments actual form data
         response = self.client.post("/request_data/?foo=whiz", data={"foo": "bang"})
         response = self.client.post("/request_data/?foo=whiz", data={"foo": "bang"})
         self.assertEqual(response.context["get-foo"], "whiz")
         self.assertEqual(response.context["get-foo"], "whiz")