Explorar o código

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

Tom Carrick hai 1 ano
pai
achega
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.
     """
 
-    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.defaults = defaults
         self.cookies = SimpleCookie()
         self.errors = BytesIO()
         if 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):
         """
@@ -459,18 +468,21 @@ class RequestFactory:
         # Refs comment in `get_bytes_from_wsgi()`.
         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."""
-        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(
             "GET",
             path,
             secure=secure,
             headers=headers,
-            **{
-                "QUERY_STRING": urlencode(data, doseq=True),
-                **extra,
-            },
+            query_params=query_params,
+            **extra,
         )
 
     def post(
@@ -481,6 +493,7 @@ class RequestFactory:
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Construct a POST request."""
@@ -494,26 +507,37 @@ class RequestFactory:
             content_type,
             secure=secure,
             headers=headers,
+            query_params=query_params,
             **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."""
-        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(
             "HEAD",
             path,
             secure=secure,
             headers=headers,
-            **{
-                "QUERY_STRING": urlencode(data, doseq=True),
-                **extra,
-            },
+            query_params=query_params,
+            **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."""
-        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(
         self,
@@ -523,11 +547,19 @@ class RequestFactory:
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         "Construct an OPTIONS request."
         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(
@@ -538,12 +570,20 @@ class RequestFactory:
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Construct a PUT request."""
         data = self._encode_json(data, content_type)
         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(
@@ -554,12 +594,20 @@ class RequestFactory:
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Construct a PATCH request."""
         data = self._encode_json(data, content_type)
         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(
@@ -570,12 +618,20 @@ class RequestFactory:
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Construct a DELETE request."""
         data = self._encode_json(data, content_type)
         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(
@@ -587,6 +643,7 @@ class RequestFactory:
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Construct an arbitrary HTTP request."""
@@ -608,6 +665,8 @@ class RequestFactory:
             )
         if headers:
             extra.update(HttpHeaders.to_wsgi_names(headers))
+        if query_params:
+            extra["QUERY_STRING"] = urlencode(query_params, doseq=True)
         r.update(extra)
         # If QUERY_STRING is absent or empty, we want to extract it from the URL.
         if not r.get("QUERY_STRING"):
@@ -685,6 +744,7 @@ class AsyncRequestFactory(RequestFactory):
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Construct an arbitrary HTTP request."""
@@ -705,18 +765,20 @@ class AsyncRequestFactory(RequestFactory):
                 ]
             )
             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
+        else:
+            # If QUERY_STRING is absent or empty, we want to extract it from
+            # the URL.
+            s["query_string"] = parsed[4]
         if headers:
             extra.update(HttpHeaders.to_asgi_names(headers))
         s["headers"] += [
             (key.lower().encode("ascii"), value.encode("latin1"))
             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)
 
 
@@ -889,7 +951,14 @@ class ClientMixin:
         return response._json
 
     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."""
         response_url = response.url
@@ -934,6 +1003,7 @@ class ClientMixin:
             content_type=content_type,
             follow=False,
             headers=headers,
+            query_params=query_params,
             **extra,
         )
 
@@ -978,9 +1048,10 @@ class Client(ClientMixin, RequestFactory):
         raise_request_exception=True,
         *,
         headers=None,
+        query_params=None,
         **defaults,
     ):
-        super().__init__(headers=headers, **defaults)
+        super().__init__(headers=headers, query_params=query_params, **defaults)
         self.handler = ClientHandler(enforce_csrf_checks)
         self.raise_request_exception = raise_request_exception
         self.exc_info = None
@@ -1042,15 +1113,23 @@ class Client(ClientMixin, RequestFactory):
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Request a response from the server using GET."""
         self.extra = extra
         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:
             response = self._handle_redirects(
-                response, data=data, headers=headers, **extra
+                response, data=data, headers=headers, query_params=query_params, **extra
             )
         return response
 
@@ -1063,6 +1142,7 @@ class Client(ClientMixin, RequestFactory):
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Request a response from the server using POST."""
@@ -1074,11 +1154,17 @@ class Client(ClientMixin, RequestFactory):
             content_type=content_type,
             secure=secure,
             headers=headers,
+            query_params=query_params,
             **extra,
         )
         if follow:
             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
 
@@ -1090,17 +1176,23 @@ class Client(ClientMixin, RequestFactory):
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Request a response from the server using HEAD."""
         self.extra = extra
         self.headers = headers
         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:
             response = self._handle_redirects(
-                response, data=data, headers=headers, **extra
+                response, data=data, headers=headers, query_params=query_params, **extra
             )
         return response
 
@@ -1113,6 +1205,7 @@ class Client(ClientMixin, RequestFactory):
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Request a response from the server using OPTIONS."""
@@ -1124,11 +1217,17 @@ class Client(ClientMixin, RequestFactory):
             content_type=content_type,
             secure=secure,
             headers=headers,
+            query_params=query_params,
             **extra,
         )
         if follow:
             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
 
@@ -1141,6 +1240,7 @@ class Client(ClientMixin, RequestFactory):
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Send a resource to the server using PUT."""
@@ -1152,11 +1252,17 @@ class Client(ClientMixin, RequestFactory):
             content_type=content_type,
             secure=secure,
             headers=headers,
+            query_params=query_params,
             **extra,
         )
         if follow:
             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
 
@@ -1169,6 +1275,7 @@ class Client(ClientMixin, RequestFactory):
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Send a resource to the server using PATCH."""
@@ -1180,11 +1287,17 @@ class Client(ClientMixin, RequestFactory):
             content_type=content_type,
             secure=secure,
             headers=headers,
+            query_params=query_params,
             **extra,
         )
         if follow:
             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
 
@@ -1197,6 +1310,7 @@ class Client(ClientMixin, RequestFactory):
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Send a DELETE request to the server."""
@@ -1208,11 +1322,17 @@ class Client(ClientMixin, RequestFactory):
             content_type=content_type,
             secure=secure,
             headers=headers,
+            query_params=query_params,
             **extra,
         )
         if follow:
             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
 
@@ -1224,17 +1344,23 @@ class Client(ClientMixin, RequestFactory):
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Send a TRACE request to the server."""
         self.extra = extra
         self.headers = headers
         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:
             response = self._handle_redirects(
-                response, data=data, headers=headers, **extra
+                response, data=data, headers=headers, query_params=query_params, **extra
             )
         return response
 
@@ -1244,6 +1370,7 @@ class Client(ClientMixin, RequestFactory):
         data="",
         content_type="",
         headers=None,
+        query_params=None,
         **extra,
     ):
         """
@@ -1257,6 +1384,7 @@ class Client(ClientMixin, RequestFactory):
                 data=data,
                 content_type=content_type,
                 headers=headers,
+                query_params=query_params,
                 **extra,
             )
             response.redirect_chain = redirect_chain
@@ -1278,9 +1406,10 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         raise_request_exception=True,
         *,
         headers=None,
+        query_params=None,
         **defaults,
     ):
-        super().__init__(headers=headers, **defaults)
+        super().__init__(headers=headers, query_params=query_params, **defaults)
         self.handler = AsyncClientHandler(enforce_csrf_checks)
         self.raise_request_exception = raise_request_exception
         self.exc_info = None
@@ -1341,17 +1470,23 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Request a response from the server using GET."""
         self.extra = extra
         self.headers = headers
         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:
             response = await self._ahandle_redirects(
-                response, data=data, headers=headers, **extra
+                response, data=data, headers=headers, query_params=query_params, **extra
             )
         return response
 
@@ -1364,6 +1499,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Request a response from the server using POST."""
@@ -1375,11 +1511,17 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
             content_type=content_type,
             secure=secure,
             headers=headers,
+            query_params=query_params,
             **extra,
         )
         if follow:
             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
 
@@ -1391,17 +1533,23 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Request a response from the server using HEAD."""
         self.extra = extra
         self.headers = headers
         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:
             response = await self._ahandle_redirects(
-                response, data=data, headers=headers, **extra
+                response, data=data, headers=headers, query_params=query_params, **extra
             )
         return response
 
@@ -1414,6 +1562,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Request a response from the server using OPTIONS."""
@@ -1425,11 +1574,17 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
             content_type=content_type,
             secure=secure,
             headers=headers,
+            query_params=query_params,
             **extra,
         )
         if follow:
             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
 
@@ -1442,6 +1597,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Send a resource to the server using PUT."""
@@ -1453,11 +1609,17 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
             content_type=content_type,
             secure=secure,
             headers=headers,
+            query_params=query_params,
             **extra,
         )
         if follow:
             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
 
@@ -1470,6 +1632,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Send a resource to the server using PATCH."""
@@ -1481,11 +1644,17 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
             content_type=content_type,
             secure=secure,
             headers=headers,
+            query_params=query_params,
             **extra,
         )
         if follow:
             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
 
@@ -1498,6 +1667,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Send a DELETE request to the server."""
@@ -1509,11 +1679,17 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
             content_type=content_type,
             secure=secure,
             headers=headers,
+            query_params=query_params,
             **extra,
         )
         if follow:
             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
 
@@ -1525,17 +1701,23 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         secure=False,
         *,
         headers=None,
+        query_params=None,
         **extra,
     ):
         """Send a TRACE request to the server."""
         self.extra = extra
         self.headers = headers
         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:
             response = await self._ahandle_redirects(
-                response, data=data, headers=headers, **extra
+                response, data=data, headers=headers, query_params=query_params, **extra
             )
         return response
 
@@ -1545,6 +1727,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
         data="",
         content_type="",
         headers=None,
+        query_params=None,
         **extra,
     ):
         """
@@ -1558,6 +1741,7 @@ class AsyncClient(ClientMixin, AsyncRequestFactory):
                 data=data,
                 content_type=content_type,
                 headers=headers,
+                query_params=query_params,
                 **extra,
             )
             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
   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
 ~~~~
 

+ 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
   for the view to function properly.
 
+.. versionchanged:: 5.1
+
+    The ``query_params`` parameter was added.
+
 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
 scope.
 
+.. versionchanged:: 5.1
+
+    The ``query_params`` parameter was added.
+
 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.
 
-.. 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.
 
@@ -129,6 +129,9 @@ Use the ``django.test.Client`` class to make requests.
 
         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
     :pep:`environ variables <3333#environ-variables>`. For example, to set the
     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,
         but the ``headers`` parameter should be preferred for readability.
 
-    The values from the ``headers`` and ``extra`` keyword arguments passed to
-    :meth:`~django.test.Client.get()`,
+    The values from the ``headers``, ``query_params``, and ``extra`` keyword
+    arguments passed to :meth:`~django.test.Client.get()`,
     :meth:`~django.test.Client.post()`, etc. have precedence over
     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 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
     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``
         object, which is documented below.
 
-        The key-value pairs in the ``data`` dictionary are used to create a GET
-        data payload. For example:
+        The key-value pairs in the ``query_params`` dictionary are used to set
+        query strings. For example:
 
         .. code-block:: pycon
 
             >>> 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:
 
@@ -177,6 +184,10 @@ Use the ``django.test.Client`` class to make requests.
 
             /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 request. For example:
 
@@ -185,7 +196,7 @@ Use the ``django.test.Client`` class to make requests.
             >>> c = Client()
             >>> c.get(
             ...     "/customers/details/",
-            ...     {"name": "fred", "age": 7},
+            ...     query_params={"name": "fred", "age": 7},
             ...     headers={"accept": "application/json"},
             ... )
 
@@ -211,8 +222,8 @@ Use the ``django.test.Client`` class to make requests.
             >>> c = Client()
             >>> c.get("/customers/details/?name=fred&age=7")
 
-        If you provide a URL with both an encoded GET data and a data argument,
-        the data argument will take precedence.
+        If you provide a URL with both an encoded GET data and either a
+        query_params or data argument these arguments will take precedence.
 
         If you set ``follow`` to ``True`` the client will follow any redirects
         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
         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
         ``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
         ``rb`` (read binary) mode.
 
-        The ``headers`` and ``extra`` parameters acts the same as for
-        :meth:`Client.get`.
+        The ``headers``, ``query_params``, and ``extra`` parameters acts the
+        same as for :meth:`Client.get`.
 
         If the URL you request with a POST contains encoded parameters, these
         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
 
-            >>> 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
         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
         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
         ``Response`` object. This method works just like :meth:`Client.get`,
-        including the ``follow``, ``secure``, ``headers``, and ``extra``
-        parameters, except it does not return a message body.
+        including the ``follow``, ``secure``, ``headers``, ``query_params``,
+        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
         ``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
         a ``Content-Type`` header is set to ``content_type``.
 
-        The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
-        the same as for :meth:`Client.get`.
+        The ``follow``, ``secure``, ``headers``, ``query_params``, and
+        ``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
         ``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
         a ``Content-Type`` header is set to ``content_type``.
 
-        The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
-        the same as for :meth:`Client.get`.
+        The ``follow``, ``secure``, ``headers``, ``query_params``, and
+        ``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
         ``Response`` object. Useful for testing RESTful interfaces.
 
-        The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
-        the same as for :meth:`Client.get`.
+        The ``follow``, ``secure``, ``headers``, ``query_params``, and
+        ``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
         ``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
         a ``Content-Type`` header is set to ``content_type``.
 
-        The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
-        the same as for :meth:`Client.get`.
+        The ``follow``, ``secure``, ``headers``, ``query_params``, and
+        ``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
         ``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
         mandates that TRACE requests must not have a body.
 
-        The ``follow``, ``secure``, ``headers``, and ``extra`` parameters act
-        the same as for :meth:`Client.get`.
+        The ``follow``, ``secure``, ``headers``, ``query_params``, and
+        ``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.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``,
 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)
 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``.
 
+.. versionchanged:: 5.1
+
+    The ``query_params`` argument was added.
+
 Using ``AsyncClient`` any method that makes a request must be awaited::
 
     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")
 
+    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(
     MIDDLEWARE=["django.middleware.csrf.CsrfViewMiddleware"],
@@ -1127,6 +1157,23 @@ class RequestFactoryTest(SimpleTestCase):
             self.assertEqual(request.headers["x-another-header"], "some other value")
             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")
 class AsyncClientTest(TestCase):
@@ -1183,6 +1230,25 @@ class AsyncClientTest(TestCase):
         response = await self.async_client.get("/post_view/")
         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")
 class AsyncRequestFactoryTest(SimpleTestCase):
@@ -1264,3 +1330,33 @@ class AsyncRequestFactoryTest(SimpleTestCase):
         request = self.request_factory.get("/somewhere/", {"example": "data"})
         self.assertNotIn("Query-String", request.headers)
         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.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
         response = self.client.post("/request_data/?foo=whiz", data={"foo": "bang"})
         self.assertEqual(response.context["get-foo"], "whiz")