Query params and secrets in app tests
diff --git a/lib/streamlit/testing/v1/app_test.py b/lib/streamlit/testing/v1/app_test.py
index b5654192c2c0..246532df3e77 100644
--- a/lib/streamlit/testing/v1/app_test.py
+++ b/lib/streamlit/testing/v1/app_test.py
@@ -22,6 +22,7 @@
 import traceback
 from typing import Any, Callable, Sequence
 from unittest.mock import MagicMock
+from urllib import parse
 
 from streamlit import source_util, util
 from streamlit.proto.WidgetStates_pb2 import WidgetStates
@@ -31,6 +32,7 @@
 )
 from streamlit.runtime.media_file_manager import MediaFileManager
 from streamlit.runtime.memory_media_file_storage import MemoryMediaFileStorage
+from streamlit.runtime.secrets import Secrets
 from streamlit.runtime.state.session_state import SessionState
 from streamlit.testing.v1.element_tree import (
     Block,
@@ -82,6 +84,7 @@ def __init__(self, script_path: str, *, default_timeout: float):
         self.default_timeout = default_timeout
         self.session_state = SessionState()
         self.query_params: dict[str, Any] = {}
+        self.secrets: dict[str, Any] = {}
 
         tree = ElementTree()
         tree._runner = self
@@ -177,6 +180,10 @@ def _run(
 
         Timeout is in seconds, or None to use the default timeout of the runner.
         """
+        # Have to import the streamlit module itself so replacing st.secrets
+        # is visible to other modules.
+        import streamlit as st
+
         if timeout is None:
             timeout = self.default_timeout
 
@@ -191,14 +198,29 @@ def _run(
             self.saved_cached_pages = source_util._cached_pages
             source_util._cached_pages = None
 
+        saved_secrets: Secrets = st.secrets
+        # Only modify global secrets stuff if we have been given secrets
+        if self.secrets:
+            new_secrets = Secrets([])
+            new_secrets._secrets = self.secrets
+            st.secrets = new_secrets
+
         with patch_config_options({"runner.postScriptGC": False}):
             script_runner = LocalScriptRunner(self._script_path, self.session_state)
             self._tree = script_runner.run(widget_state, self.query_params, timeout)
             self._tree._runner = self
+            # Last event is SHUTDOWN, so the corresponding data includes query string
+            query_string = script_runner.event_data[-1]["client_state"].query_string
+            self.query_params = parse.parse_qs(query_string)
 
         # teardown
         with source_util._pages_cache_lock:
             source_util._cached_pages = self.saved_cached_pages
+
+        if self.secrets:
+            if st.secrets._secrets is not None:
+                self.secrets = dict(st.secrets._secrets)
+            st.secrets = saved_secrets
         Runtime._instance = None
 
         return self
diff --git a/lib/tests/streamlit/testing/test_runner_test.py b/lib/tests/streamlit/testing/test_runner_test.py
index 5ac15c19d110..9842ee0ce5af 100644
--- a/lib/tests/streamlit/testing/test_runner_test.py
+++ b/lib/tests/streamlit/testing/test_runner_test.py
@@ -42,7 +42,7 @@ def test_from_file():
     script.run()
 
 
-def test_query_params():
+def test_get_query_params():
     def script():
         import streamlit as st
 
@@ -54,3 +54,27 @@ def script():
     at.query_params["bar"] = "baz"
     at.run()
     assert at.get("json")[0].body == '{"foo": ["5"], "bar": ["baz"]}'
+
+
+def test_set_query_params():
+    def script():
+        import streamlit as st
+
+        st.experimental_set_query_params(foo="bar")
+
+    at = AppTest.from_function(script).run()
+    # parse.parse_qs puts everything in lists
+    assert at.query_params["foo"] == ["bar"]
+
+
+def test_secrets():
+    def script():
+        import streamlit as st
+
+        st.write(st.secrets["foo"])
+
+    at = AppTest.from_function(script)
+    at.secrets["foo"] = "bar"
+    at.run()
+    assert at.markdown[0].value == "bar"
+    assert at.secrets["foo"] == "bar"

Query param reading, plus somewhat hacky secrets support.

## Testing Plan

Some tests added, but secrets are undertested

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.
