Repository: topsport-com-au/starlite-saqlalchemy (language: python)

Issue:
ControllerTest: skip testing methods that aren't registered
There is no way to either explicitly or implicitly skip testing methods for which there is no route handler registered.


Discussion / hints:

Fix patch:
diff --git a/.flake8 b/.flake8
index 6f3cd761..f7517662 100644
--- a/.flake8
+++ b/.flake8
@@ -6,12 +6,13 @@ type-checking-exempt-modules = from sqlalchemy.orm
 per-file-ignores =
     examples/dto/*:T201,TC
     examples/tests/*:SCS108,PT013
+    src/pytest_starlite_saqlalchemy/__init__.py:F401
     src/starlite_saqlalchemy/dependencies.py:TC
     src/starlite_saqlalchemy/health.py:TC
     src/starlite_saqlalchemy/repository/filters.py:TC
     src/starlite_saqlalchemy/scripts.py:T201
     src/starlite_saqlalchemy/settings.py:TC
-    src/starlite_saqlalchemy/testing.py:SCS108
+    src/starlite_saqlalchemy/testing/controller_test.py:SCS108
     src/starlite_saqlalchemy/users/controllers.py:TC
     tests/*:SCS108,PT013
     tests/integration/test_tests.py:TC002,SCS108
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8e1b5f8c..a288b8d5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,7 +4,6 @@ on:
   push:
     branches: [main]
   pull_request:
-    branches: [main]
 
 env:
   FORCE_COLOR: "1"
@@ -35,7 +34,7 @@ jobs:
         with:
           python-version: ${{env.PYTHON_LATEST}}
       - name: Install Dependencies
-        run: python -m pip install --upgrade wheel tox
+        run: python -m pip install --upgrade -r requirements.ci.txt
       - run: python -m tox -e pylint
   mypy:
     name: mypy
@@ -56,7 +55,7 @@ jobs:
         with:
           python-version: ${{env.PYTHON_LATEST}}
       - name: Install Dependencies
-        run: python -m pip install --upgrade wheel tox
+        run: python -m pip install --upgrade -r requirements.ci.txt
       - run: python -m tox -e mypy
   pyright:
     name: pyright
@@ -78,7 +77,7 @@ jobs:
         with:
           python-version: ${{env.PYTHON_LATEST}}
       - name: Install Dependencies
-        run: python -m pip install --upgrade wheel tox
+        run: python -m pip install --upgrade -r requirements.ci.txt
       - run: python -m tox -e pyright
   tests:
     name: tests on ${{matrix.python-version}}
@@ -108,7 +107,7 @@ jobs:
         with:
           python-version: ${{matrix.python-version}}
       - name: Install Dependencies
-        run: python -m pip install --upgrade wheel tox tox-gh-actions
+        run: python -m pip install --upgrade -r requirements.ci.txt tox-gh-actions
       - run: python -m tox
       - name: Upload Coverage Data
         uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # @v3.1.1
@@ -133,7 +132,7 @@ jobs:
       - uses: actions/setup-python@5ccb29d8773c3f3f653e1705f474dfaa8a06a912 #v4.4.0
         with:
           python-version: ${{env.PYTHON_LATEST}}
-      - run: python -m pip install --upgrade wheel tox
+      - run: python -m pip install --upgrade -r requirements.ci.txt
       - name: Download coverage data
         uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # @v3.0.1
         with:
diff --git a/mkdocs.yml b/mkdocs.yml
index 763726ef..cc396d14 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -7,6 +7,8 @@ nav:
     - DTOs: dto.md
     - Async Worker: async_worker.md
     - Logging: logging.md
+    - Testing:
+      - Pytest Plugin: testing/pytest_plugin.md
   - Reference: reference/
 watch:
   - src/starlite_saqlalchemy
diff --git a/poetry.lock b/poetry.lock
index a910e534..ae8e4410 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -140,22 +140,23 @@ python-dateutil = "*"
 
 [[package]]
 name = "dnspython"
-version = "2.2.1"
+version = "2.3.0"
 description = "DNS toolkit"
 category = "main"
 optional = false
-python-versions = ">=3.6,<4.0"
+python-versions = ">=3.7,<4.0"
 files = [
-    {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"},
-    {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"},
+    {file = "dnspython-2.3.0-py3-none-any.whl", hash = "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46"},
+    {file = "dnspython-2.3.0.tar.gz", hash = "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9"},
 ]
 
 [package.extras]
 curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"]
-dnssec = ["cryptography (>=2.6,<37.0)"]
-doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"]
+dnssec = ["cryptography (>=2.6,<40.0)"]
+doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.11.0)"]
+doq = ["aioquic (>=0.9.20)"]
 idna = ["idna (>=2.1,<4.0)"]
-trio = ["trio (>=0.14,<0.20)"]
+trio = ["trio (>=0.14,<0.23)"]
 wmi = ["wmi (>=1.5.1,<2.0.0)"]
 
 [[package]]
@@ -176,14 +177,14 @@ idna = ">=2.0.0"
 
 [[package]]
 name = "faker"
-version = "15.3.4"
+version = "16.4.0"
 description = "Faker is a Python package that generates fake data for you."
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "Faker-15.3.4-py3-none-any.whl", hash = "sha256:c2a2ff9dd8dfd991109b517ab98d5cb465e857acb45f6b643a0e284a9eb2cc76"},
-    {file = "Faker-15.3.4.tar.gz", hash = "sha256:2d5443724f640ce07658ca8ca8bbd40d26b58914e63eec6549727869aa67e2cc"},
+    {file = "Faker-16.4.0-py3-none-any.whl", hash = "sha256:5420467fad3fa582094057754e5e81326cb1f51ab822bf9df96c077cfb35ae49"},
+    {file = "Faker-16.4.0.tar.gz", hash = "sha256:dcffdca8ec9a715982bcd5f53ee688dc4784cd112f9910f8f7183773eb3ec276"},
 ]
 
 [package.dependencies]
@@ -191,30 +192,33 @@ python-dateutil = ">=2.4"
 
 [[package]]
 name = "fast-query-parsers"
-version = "0.2.0"
+version = "0.3.0"
 description = "Ultra-fast query string and url-encoded form-data parsers"
 category = "main"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "fast_query_parsers-0.2.0-cp38-abi3-macosx_10_7_x86_64.whl", hash = "sha256:4f7219e1dc7480307a32fae000470c2b8979849a34395d7787898da23e7d6a7e"},
-    {file = "fast_query_parsers-0.2.0-cp38-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:301154360a39b1dc347e9a9e0cb3578c8877f63bb96367d54e8012b81a4007b3"},
-    {file = "fast_query_parsers-0.2.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1556b7ab6614525d5697e9f46bd56200de505e6423ee99df38f88163df7053d"},
-    {file = "fast_query_parsers-0.2.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fdec68138ed327f1b893e0a78156a8b2fcfc0753cc26ea28da53d578801ef86f"},
-    {file = "fast_query_parsers-0.2.0-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:03520ba888acfd8a26a1a4cfa60c499e29db5d4d2fdcd1a8f393c786ad493d13"},
-    {file = "fast_query_parsers-0.2.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9764839c444c025aaad114373e9b1e6eafd16e32321663a870811358f9d65f18"},
-    {file = "fast_query_parsers-0.2.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9d86c9e09f1eebb6fa161792f6565b8b4821211ef58f054f3183c210ac01a21"},
-    {file = "fast_query_parsers-0.2.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ffaf0cde56652c2b40ec9292b4abd79c2a76a0f095ffb22dbead6129f47209e"},
-    {file = "fast_query_parsers-0.2.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:862705292b003d3c1f0d0219be874b586e71a3e9bc462ad24493c7de3d7ca53b"},
-    {file = "fast_query_parsers-0.2.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:51335ffed2d835e5f679f8f01c593e8477b21a6e6bc90a0caa9e1eb66016e533"},
-    {file = "fast_query_parsers-0.2.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:8e55c12af6235d04cd1ed0ddfbcf959bd1265b0bf6a8fa5b1481f47feb44bed7"},
-    {file = "fast_query_parsers-0.2.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:325f267013388b902f9d0a95a84a128c3aeb2f1b252494e7f516dfd433c7e5a8"},
-    {file = "fast_query_parsers-0.2.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2916e7fe50b91c9f0576af7c6b06677e6634a7017967fe3d2e9030a86a650afa"},
-    {file = "fast_query_parsers-0.2.0-cp38-abi3-win32.whl", hash = "sha256:a5d262709e2f08a5f953070160eabe7ca3c909985026b00ee3c7d45d0bf9b9d6"},
-    {file = "fast_query_parsers-0.2.0-cp38-abi3-win_amd64.whl", hash = "sha256:1a0d8b3435a6ab51ac8eb3a96eab8fc715b09e7e196ec74af48ac4b126f3d4bc"},
-    {file = "fast_query_parsers-0.2.0.tar.gz", hash = "sha256:3a0105dc3f5c39befe89aaeb5ac0e71e02f9854f75fd09b4b13775085be44ab5"},
+    {file = "fast_query_parsers-0.3.0-cp38-abi3-macosx_10_7_x86_64.whl", hash = "sha256:e01e9b794d6fad11207341a8e0a7b179f085c9874b3981a77f862a5e44cb241d"},
+    {file = "fast_query_parsers-0.3.0-cp38-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:f2ffa7f1298db8ff8e013733e32edb77509a468843f5641875386f1df9bdecb4"},
+    {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1becd3a0a06b14c57ee34976bf54d08fc910d4b410170ee5c28ac8d16ef23575"},
+    {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd94f7715b6c1ec57b50e63c22298253648d864c9e78f90a258ebf2c07d13a38"},
+    {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:52ef0241bcadc93ef47f650070c5f31f616cd5185a3af95f8b365e1fb135d991"},
+    {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:189eed1007229c1e4a032076d503e92869eebd4a1ebd2498e6bb1b1b0c525708"},
+    {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97d13dcc6cad29c8ef7c6f4bb6240847b476e4eb0445ac7f59d49a11db1644e2"},
+    {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:befa91ff707f77e9948759bafdfe2b743d4b587e3f7767f2b5887b1ffa37f41d"},
+    {file = "fast_query_parsers-0.3.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00d3d519cfd020c7a3bd961c9568b6042f853f25e204b8a307ff5b569dee9c38"},
+    {file = "fast_query_parsers-0.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a659eb2acc0aa44f4cce7a79c4e0c76f14a26026fc13db8f094a29a38cf66248"},
+    {file = "fast_query_parsers-0.3.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e9b277e8281d7fe9b8e9450767cf1c1158ed729d81e03c1dfa2fe2118b22ffe0"},
+    {file = "fast_query_parsers-0.3.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:0395ef9cacf49318a0f3caac4fde8a01dd7b7315c14f8a7d084965dedd764868"},
+    {file = "fast_query_parsers-0.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:47639fc331e668b1be405c4e24af70ad21fc01a6847d2730803346ab9586e788"},
+    {file = "fast_query_parsers-0.3.0-cp38-abi3-win32.whl", hash = "sha256:89ee582ab4b331f078eca2fbf51ce82396f749879fc30f20668b85a2896ea165"},
+    {file = "fast_query_parsers-0.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:4e9dd270f085a2337fab407d230a252664f952561cb83acece343a5c6a7d8e6b"},
+    {file = "fast_query_parsers-0.3.0.tar.gz", hash = "sha256:df972c0b58d0bf51fa43b67d2604ab795984015d47552d02175ebcc685e4852b"},
 ]
 
+[package.dependencies]
+maturin = "*"
+
 [[package]]
 name = "greenlet"
 version = "2.0.1"
@@ -303,100 +307,101 @@ files = [
 
 [[package]]
 name = "hiredis"
-version = "2.1.0"
+version = "2.1.1"
 description = "Python wrapper for hiredis"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "hiredis-2.1.0-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:7b339a7542a3f6a10b3bbc157e4abc9bae9628e2df7faf5f8a32f730014719ae"},
-    {file = "hiredis-2.1.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:dd82370c2f9f804ec617b95d25edb0fd04882251afb2ecdf08b9ced0c3aa4bcc"},
-    {file = "hiredis-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:92077511d3a62109d5d11bf584e41264a993ae3c77c72de63c1f741b7809bacb"},
-    {file = "hiredis-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6544c7807cbb75bc6ae9ab85773b4413edbcd55342e9e3d7d3f159f677f7428"},
-    {file = "hiredis-2.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8181d73f25943fbdca904154e51b845317103cee08116cfae258f96927ce1e74"},
-    {file = "hiredis-2.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040f861e4e43daa9287f3a85979542f9c7ee8cfab695fa662f3b6186c6f7d5e8"},
-    {file = "hiredis-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef5ae8c1af82a8000742003cb16a6fa6c57919abb861ab214dcb27db8573ee64"},
-    {file = "hiredis-2.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b9aa1b0ec46dec5b05dcec22e50bbd4af33da121fca83bd2601dc60c79183f9"},
-    {file = "hiredis-2.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c53c36a630a6c6fd9dfe439f4266e564ca58995015a780c1d964567ebf328466"},
-    {file = "hiredis-2.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:05aab35210bd7fbd7bd066efb2a42eb5c2878c2c137a9cff597204be2c07475b"},
-    {file = "hiredis-2.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e6097e1cef647c665f71cd0e58346389580db98365e804f7a9ad5d96e66b7150"},
-    {file = "hiredis-2.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:32f98370efed38088d000df2eb2c8ed43d93d99bbf4a0a740e15eb4a887cc23f"},
-    {file = "hiredis-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b85276ed57e0aee8910b48383a38a299851935ba134460bad394988c750985fe"},
-    {file = "hiredis-2.1.0-cp310-cp310-win32.whl", hash = "sha256:bd9d99606008a8cfa6b9e950abaa35f5b87496f03e63b73197d02b0fe7ecb6d3"},
-    {file = "hiredis-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:6a8e796c94b7b8c63c99757d6ec2075069e4c362dfb0f130aaf874422bea3e7d"},
-    {file = "hiredis-2.1.0-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:e7bb5cab604fc45b45cee40e84e84d9e30eeb34c571a3784392ae658273bbd23"},
-    {file = "hiredis-2.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e0d4b074ff5ebba00933da27a06f3752b8af2448a6aa9dc895d5279f43011530"},
-    {file = "hiredis-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f0c2dbaffd4a9e8df04731a012c8a67b7517abec7e53bb12c3cd749865c63428"},
-    {file = "hiredis-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c19151e79b36e0d849899a21fc10539aa1903af94b31754bddab1bea876cd508"},
-    {file = "hiredis-2.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:08ec41519a533f5cd1f1f8bd1797929358117c8e4570b679b469f768b45b7dbf"},
-    {file = "hiredis-2.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f0db3667fa8abbd37ac66385b460841029033bfc1ba8d7e5b3ff1e01d3346a"},
-    {file = "hiredis-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f592d1522b5981890b34b0b814f4bfa4a68b23ee90f538aac321d17e8bf859c8"},
-    {file = "hiredis-2.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dddd2be67de25a62b3bf871f091181c13da3b32186d4be6af49dadbf6fdc266d"},
-    {file = "hiredis-2.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4ee8f6d0774cd6179c625688201e961a2d03da212230adaa2193cfb7a04f9169"},
-    {file = "hiredis-2.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5000942ffb6b6410ccbc87089c15fde5f48bd205664ee8b3067e6b2fb5689485"},
-    {file = "hiredis-2.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:21e0017b8f50abd13b4c4c4218c7dfd5a42623e3255b460dfa5f70b45c4e7c3e"},
-    {file = "hiredis-2.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:40b55fb46fcc78b04190176c0ae28bfa3cc7f418fca9df06c037028af5942b6a"},
-    {file = "hiredis-2.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:24a55169a7f0bd9458935ac644bf8191f127c8aa50cdd70c0b87928cc515cae5"},
-    {file = "hiredis-2.1.0-cp311-cp311-win32.whl", hash = "sha256:bb60f79e8c1eb5971b10fd256764ea0c89c4ad2d55ac4379981f678f349411f2"},
-    {file = "hiredis-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:b223668844f26034759a6c24a72f0bb8e4fb64a43b27e2f3e8378639eaac1661"},
-    {file = "hiredis-2.1.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:7f7e7d91d6533fcb1939d467cf8bfb98640edf715897959f31ae83f5ad29aed3"},
-    {file = "hiredis-2.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:531d1d3955244831b69272b993e16f93489ce2dadfdf800ac856dc2d9a43d353"},
-    {file = "hiredis-2.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66ffcbfc4db52dd87cdfd53bda45881ab3ab07c80ec43244fd8d70ee69d42c01"},
-    {file = "hiredis-2.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:023b3b3ac410d6cfdb45ee943b8c528c90379f31419a1fd229888aa2b965732d"},
-    {file = "hiredis-2.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c972385a0647120d4b0fe0e9567257cad7b2577b9f1315815713c571af0e778d"},
-    {file = "hiredis-2.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32893825426e73d57b3290b68110dd76229945e6c79b08a37795f536501935c4"},
-    {file = "hiredis-2.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:262148f9b616c0cdd0f2c6bda45cd0f1ce6ce2d1974efd296b85b44e5c7567c2"},
-    {file = "hiredis-2.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d601c27b9599fe52cade3096351f92f665e527d29af8d3e29353a76bfcf5615"},
-    {file = "hiredis-2.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d248acc7d7713c1b3d48ed8ea67d6ba43b104aa67d63078846a3590adbab6b73"},
-    {file = "hiredis-2.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:969ffe37a8980a6e5404993ccfe605a40fa6732fa6d7b26a1a718c9121197002"},
-    {file = "hiredis-2.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:288d5d0566d3cbcd800e46c7a547428d321842898b8c7de037a7e78b5644e88a"},
-    {file = "hiredis-2.1.0-cp37-cp37m-win32.whl", hash = "sha256:06cb776d3cd3cbec86010f1bab6895ee16af8036aae8c3594a5e96c24f0f83a5"},
-    {file = "hiredis-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6766376dc43ef186113422ecacec0ece0d4b12c0e5f4b556669e639b20ccabb1"},
-    {file = "hiredis-2.1.0-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:41afba30304adcbe1c93fc8272a7169b7fc4e4d3d470ad8babd391678a519d76"},
-    {file = "hiredis-2.1.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6df0115f8b0766cd3d12416e2e2e914efed5b1a1a27605c9f37bc92de086877a"},
-    {file = "hiredis-2.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d7d7078f3b841ad86e35459e9f1a49db6d793b796a25fe866333166196d9fec"},
-    {file = "hiredis-2.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:835c4cbf8b38c83240b3eb9bd575cd1bfefe5ea5c46cc5ac2bf2d1f47d1fd696"},
-    {file = "hiredis-2.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:718589c48e97820bdc2a99e2621b5039884cc23199213756054d10cd309ad56c"},
-    {file = "hiredis-2.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2d96be6917ea8f753691a4674f682dd5e145b70edab28c05aa5552ae873e843"},
-    {file = "hiredis-2.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5fe1bb4b1525751f3050337097b3b2bfe445836e59a5a0984928dd0797f9abf"},
-    {file = "hiredis-2.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91dc73310b92b4aeccffdcd4a762955fe71380f5eaa4e242ee95019e41519101"},
-    {file = "hiredis-2.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bb858218de60a930a164a991fff001c70b0c3d923d3ae40fef2acf3321126b00"},
-    {file = "hiredis-2.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:53040c3b3488b52f4609775453fc759262f2885b733150ee2e1d88257fdafed8"},
-    {file = "hiredis-2.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a1c9b7d6d7bf35e1e2217b2847710154b11d25bf86b77bb7e190161f8b89917e"},
-    {file = "hiredis-2.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:dfbe939fdddbc7b90cab4124f3ddd6391099fb964f6dab3386aa8cf56f37b5ba"},
-    {file = "hiredis-2.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3a51cb4ea466276a845a940931357b4a876f903eabde514ba95e45050e1c2150"},
-    {file = "hiredis-2.1.0-cp38-cp38-win32.whl", hash = "sha256:8bce4c687136bf13df76072072b9baadbd52f7d1b143fbbda96387f50e8ebaeb"},
-    {file = "hiredis-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:1f94684b13fbbee1239303018d5ea900d786e486cdb130cde3144d53f4e262e4"},
-    {file = "hiredis-2.1.0-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:879668ffab582bdffd9f10f6c8797aac055db183f266e3aa3a6438ff0768bc29"},
-    {file = "hiredis-2.1.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:f1d5a99de0fd02438f251e50ec64936d22d542c8e5d80bdec236f9713eeef334"},
-    {file = "hiredis-2.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab622bcddcf334b4b1fc4b22e163e93160e3afdd7feaedd77ac6f258e0c77b68"},
-    {file = "hiredis-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:964c4f23ff450fb8d73edf06fc7475a4e81a3f9b03a9a04a907ec81c84052fcf"},
-    {file = "hiredis-2.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9f8b8daef346ffc0268d7086c213ab24c2a3fcbd4249eacfbb3635602c79d20"},
-    {file = "hiredis-2.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e2039cdaa2e6656eae4a2e2537ed77e27f29b7487b97ce7ae6a3cb88d01b968"},
-    {file = "hiredis-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43d3168da0a81fa0a9e4bc6e14316beac8e5f1b439ca5cc5af7f9a558cfba741"},
-    {file = "hiredis-2.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0021ba034b74c5006f62e4cfdd79d04c7c720731eda256ce29d769ac6483adc3"},
-    {file = "hiredis-2.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:39a1bb45bcd698baf70ad4e9a94af164525bf053caea7df3777172d20d69538a"},
-    {file = "hiredis-2.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c1b636b05777536a83b4cced157cbdc2d0012d494a9ec2f7b7e07c54296cd773"},
-    {file = "hiredis-2.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:58a7ceb71f967fcc1878fb64666a12fbc5f243ab00d0653d3752a811941d8261"},
-    {file = "hiredis-2.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c5263c676dc4d55202e7ca0429b949fc6ba7c0dd3a3a2b80538593ab27d82836"},
-    {file = "hiredis-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b5879d13025b04903ddf71921812db27fe1156a0952ad253014354d72463aaa9"},
-    {file = "hiredis-2.1.0-cp39-cp39-win32.whl", hash = "sha256:9259f637d77544ffeb97acb0a87fdd192a8aced7a2fbd7439160dbee8341d446"},
-    {file = "hiredis-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:fb818b6e0981e16dfdfc9e507c9842f8d210e6ecaf3edb8ac3039dbd24768839"},
-    {file = "hiredis-2.1.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:648d4648bf6b3dcc418a974df143b2f96627ab8b50bda23a57759c273880ecfb"},
-    {file = "hiredis-2.1.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:654949cfc0bc76a5292b6ac111113b2eafb0739e0496495368981ea2e80bf4ec"},
-    {file = "hiredis-2.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2a98b835c2088998a47da51b1b3661b587b2d4b3305d03fc9893888cc2aa54"},
-    {file = "hiredis-2.1.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7222bd9243387d778245619d0ac62d35cf72ee746ec0efb7b9b230ae3e0c3a39"},
-    {file = "hiredis-2.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:778f6de73c3abd67d447a3442f89e7d43a8de1eb5093f416af14dddc1d5c9cb5"},
-    {file = "hiredis-2.1.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c4cfb61fe642f30a22789055847004393bc65b5686988c64191e379ea4ccd069"},
-    {file = "hiredis-2.1.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03b6bef7eb50415aca87200a511d66a2fd69f1fcc75cfe1408e1201cbe28ddfb"},
-    {file = "hiredis-2.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3195e13a700f6ff35894c4920fcce8f6c2b01cdbc01f76fe567753c495849e9b"},
-    {file = "hiredis-2.1.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19f724405c808a89db422ed1010caab80a16d3e5b49632356ae7912513b6d58e"},
-    {file = "hiredis-2.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8ecebeff966b412138b0cd105d7572f8d5e65e96355af699863890f8370707e6"},
-    {file = "hiredis-2.1.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4f34eefaf164bf43b29ccc809c168248eb95001837ed0e9e3279891f57ae2fab"},
-    {file = "hiredis-2.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11fad16beb9d623ea423c9129bab0e392ea4c84363d61c125f679be3d029442f"},
-    {file = "hiredis-2.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c763eb9a1414c4d665945c70ae2ef74a843600667b0069fe90e2aabc78e5411"},
-    {file = "hiredis-2.1.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edb7f156a8f8a1999574f27bda67dd2bff2d5b180bb6aed996a1792cafbcc668"},
-    {file = "hiredis-2.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e057d5545189d4c9e22ae0f7dc283ea0a225f56999511022c062cce7f9589d69"},
+    {file = "hiredis-2.1.1-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:f15e48545dadf3760220821d2f3c850e0c67bbc66aad2776c9d716e6216b5103"},
+    {file = "hiredis-2.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b3a437e3af246dd06d116f1615cdf4e620e639dfcc923fe3045e00f6a967fc27"},
+    {file = "hiredis-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b61732d75e2222a3b0060b97395df78693d5c3487fe4a5d0b75f6ac1affc68b9"},
+    {file = "hiredis-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:170c2080966721b42c5a8726e91c5fc271300a4ac9ddf8a5b79856cfd47553e1"},
+    {file = "hiredis-2.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2d6e4caaffaf42faf14cfdf20b1d6fff6b557137b44e9569ea6f1877e6f375d"},
+    {file = "hiredis-2.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d64b2d90302f0dd9e9ba43e89f8640f35b6d5968668da82ba2d2652b2cc3c3d2"},
+    {file = "hiredis-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61fd1c55efb48ba734628f096e7a50baf0df3f18e91183face5c07fba3b4beb7"},
+    {file = "hiredis-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfc5e923828714f314737e7f856b3dccf8805e5679fe23f07241b397cd785f6c"},
+    {file = "hiredis-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ef2aa0485735c8608a92964e52ab9025ceb6003776184a1eb5d1701742cc910b"},
+    {file = "hiredis-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2d39193900a03b900a25d474b9f787434f05a282b402f063d4ca02c62d61bdb9"},
+    {file = "hiredis-2.1.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:4b51f5eb47e61c6b82cb044a1815903a77a4f840fa050fd2ff40d617c102d16c"},
+    {file = "hiredis-2.1.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d9145d011b74bef972b485a09f391babaa101626dbb54afc2313d5682a746593"},
+    {file = "hiredis-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6f45509b43d720d64837c1211fcdea42acd48e71539b7152d74c16413ceea080"},
+    {file = "hiredis-2.1.1-cp310-cp310-win32.whl", hash = "sha256:3a284bbf6503cd6ac1183b3542fe853a8be47fb52a631224f6dda46ba229d572"},
+    {file = "hiredis-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:f60fad285db733b2badba43f7036a1241cb3e19c17260348f3ff702e6eaa4980"},
+    {file = "hiredis-2.1.1-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:69c20816ac2af11701caf10e5b027fd33c6e8dfe7806ab71bc5191aa2a6d50f9"},
+    {file = "hiredis-2.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cd43dbaa73322a0c125122114cbc2c37141353b971751d05798f3b9780091e90"},
+    {file = "hiredis-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c9632cd480fbc09c14622038a9a5f2f21ef6ce35892e9fa4df8d3308d3f2cedf"},
+    {file = "hiredis-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:252d4a254f1566012b94e35cba577a001d3a732fa91e824d2076233222232cf9"},
+    {file = "hiredis-2.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b901e68f3a6da279388e5dbe8d3bc562dd6dd3ff8a4b90e4f62e94de36461777"},
+    {file = "hiredis-2.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f45f296998043345ecfc4f69a51fa4f3e80ca3659864df80b459095580968a6"},
+    {file = "hiredis-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79f2acf237428dd61faa5b49247999ff68f45b3552c57303fcfabd2002eab249"},
+    {file = "hiredis-2.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82bc6f5b92c9fcd5b5d6506000dd433006b126b193932c52a9bcc10dcc10e4fc"},
+    {file = "hiredis-2.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:19843e4505069085301c3126c91b4e48970070fb242d7c617fb6777e83b55541"},
+    {file = "hiredis-2.1.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c7336fddae533cbe786360d7a0316c71fe96313872c06cde20a969765202ab04"},
+    {file = "hiredis-2.1.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:90b4355779970e121c219def3e35533ec2b24773a26fc4aa0f8271dd262fa2f2"},
+    {file = "hiredis-2.1.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4beaac5047317a73b27cf15b4f4e0d2abaafa8378e1a6ed4cf9ff420d8f88aba"},
+    {file = "hiredis-2.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7e25dc06e02689a45a49fa5e2f48bdfdbc11c5b52bef792a8cb37e0b82a7b0ae"},
+    {file = "hiredis-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8b3233c1de155743ef34b0cae494e33befed5e0adba77762f5d8a8e417c5015"},
+    {file = "hiredis-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:4ced076af04e28761d486501c58259247c1882fd19c7f94c18a257d143248eee"},
+    {file = "hiredis-2.1.1-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:f4300e063045e11ee79b79a7c9426813ab8d97e340b15843374093225dde407d"},
+    {file = "hiredis-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b04b6c04fe13e1e30ba6f9340d3d0fb776a7e52611d11809fb59341871e050e5"},
+    {file = "hiredis-2.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:436dcbbe3104737e8b4e2d63a019a764d107d72d6b6ee3cd107097c1c263fd1e"},
+    {file = "hiredis-2.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:11801d9e96f39286ab558c6db940c39fc00150450ae1007d18b35437d2f79ad7"},
+    {file = "hiredis-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7d8d0ca7b4f6136f8a29845d31cfbc3f562cbe71f26da6fca55aa4977e45a18"},
+    {file = "hiredis-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c040af9eb9b12602b4b714b90a1c2ac1109e939498d47b0748ec33e7a948747"},
+    {file = "hiredis-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f448146b86a8693dda5f02bb4cb2ef65c894db2cf743e7bf351978354ce685e3"},
+    {file = "hiredis-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:649c5a1f0952af50f008f0bbec5f0b1e519150220c0a71ef80541a0c128d0c13"},
+    {file = "hiredis-2.1.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b8e7415b0952b0dd6df3aa2d37b5191c85e54d6a0ac1449ddb1e9039bbb39fa5"},
+    {file = "hiredis-2.1.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:38c1a56a30b953e3543662f950f498cfb17afed214b27f4fc497728fb623e0c9"},
+    {file = "hiredis-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6050b519fb3b62d68a28a1941ae9dc5122e8820fef2b8e20a65cb3c1577332a0"},
+    {file = "hiredis-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:96add2a205efffe5e19a256a50be0ed78fcb5e9503242c65f57928e95cf4c901"},
+    {file = "hiredis-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:8ceb101095f8cce9ac672ed7244b002d83ea97af7f27bb73f2fbe7fe8e8f03c7"},
+    {file = "hiredis-2.1.1-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:9f068136e5119f2ba939ecd45c47b4e3cf6dd7ca9a65b6078c838029c5c1f564"},
+    {file = "hiredis-2.1.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:8a42e246a03086ae1430f789e37d7192113db347417932745c4700d8999f853a"},
+    {file = "hiredis-2.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5359811bfdb10fca234cba4629e555a1cde6c8136025395421f486ce43129ae3"},
+    {file = "hiredis-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d304746e2163d3d2cbc4c08925539e00d2bb3edc9e79fce531b5468d4e264d15"},
+    {file = "hiredis-2.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4fe297a52a8fc1204eef646bebf616263509d089d472e25742913924b1449099"},
+    {file = "hiredis-2.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:637e563d5cbf79d8b04224f99cfce8001146647e7ce198f0b032e32e62079e3c"},
+    {file = "hiredis-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39b61340ff2dcd99d5ded0ca5fc33c878d89a1426e2f7b6dbc7c7381e330bc8a"},
+    {file = "hiredis-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66eaf6d5ea5207177ba8ffb9ee479eea743292267caf1d6b89b51cf9d5885d23"},
+    {file = "hiredis-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4d2d0e458c32cdafd9a0f0b0aaeb61b169583d074287721eee740b730b7654bd"},
+    {file = "hiredis-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8a92781e466f2f1f9d38720d8920cb094bc0d59f88219591bc12b1c12c9d471c"},
+    {file = "hiredis-2.1.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:5560b09304ebaac5323a7402f5090f2a8559843200014f5adf1ff7517dd3805b"},
+    {file = "hiredis-2.1.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4732a0bf877bbd69d4d1b38a3db2160252acb31894a48f324fd54f742f6b2123"},
+    {file = "hiredis-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b5bd33ac8a572e2aa94b489dec35b0c00ca554b27e56ad19953e0bf2cbcf3ad8"},
+    {file = "hiredis-2.1.1-cp38-cp38-win32.whl", hash = "sha256:07e86649773e486a21e170d1396217e15833776d9e8f4a7121c28a1d37e032c9"},
+    {file = "hiredis-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:b964d81db8f11a99552621acd24c97381a0fd401a57187ce9f8cb9a53f4b6f4e"},
+    {file = "hiredis-2.1.1-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:27e89e7befc785a273cccb105840db54b7f93005adf4e68c516d57b19ea2aac2"},
+    {file = "hiredis-2.1.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ea6f0f98e1721741b5bc3167a495a9f16459fe67648054be05365a67e67c29ba"},
+    {file = "hiredis-2.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:40c34aeecccb9474999839299c9d2d5ff46a62ed47c58645b7965f48944abd74"},
+    {file = "hiredis-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65927e75da4265ec88d06cbdab20113a9e69bbac3aea1ec053d4d940f1c88fc8"},
+    {file = "hiredis-2.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72cab67bcceb2e998da2f28aad9ec7b1a5ece5888f7ac3d3723cccba62338703"},
+    {file = "hiredis-2.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d67429ff99231137491d8c3daa097c767a9c273bb03ac412ed8f6acb89e2e52f"},
+    {file = "hiredis-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c596bce5e9dd379c68c17208716da2767bb6f6f2a71d748f9e4c247ced31e6"},
+    {file = "hiredis-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e0aab2d6e60aa9f9e14c83396b4a58fb4aded712806486c79189bcae4a175ac"},
+    {file = "hiredis-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:17deb7d218a5ae9f05d2b19d51936231546973303747924fc17a2869aef0029a"},
+    {file = "hiredis-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d3d60e2af4ce93d6e45a50a9b5795156a8725495e411c7987a2f81ab14e99665"},
+    {file = "hiredis-2.1.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:fbc960cd91e55e2281e1a330e7d1c4970b6a05567dd973c96e412b4d012e17c6"},
+    {file = "hiredis-2.1.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0ae718e9db4b622072ff73d38bc9cd7711edfedc8a1e08efe25a6c8170446da4"},
+    {file = "hiredis-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e51e3fa176fecd19660f898c4238232e8ca0f5709e6451a664c996f9aec1b8e1"},
+    {file = "hiredis-2.1.1-cp39-cp39-win32.whl", hash = "sha256:0258bb84b4a1e015f14f891d91957042fa88f6f4e86cc0808d735ebbc1e3fc88"},
+    {file = "hiredis-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:c5a47c964c58c044a323336a798d8729722e09865d7e087eb3512df6146b39a8"},
+    {file = "hiredis-2.1.1-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8de0334c212e069d49952e476e16c6b42ba9677cc1e2d2f4588bd9a39489a3ab"},
+    {file = "hiredis-2.1.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:653e33f69202c00eca35416ee23091447ad1e9f9a556cc2b715b2befcfc31b3c"},
+    {file = "hiredis-2.1.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14cccf931c859ba3169d766e892a3673a79649ec2ceca7ba95ea376b23fd222"},
+    {file = "hiredis-2.1.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86c56359fd7aca6a9ca41af91636aef15d5ad6d19e631ebd662f233c79f7e100"},
+    {file = "hiredis-2.1.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:c2b197e3613c3aef3933b2c6eb095bd4be9c84022aea52057697b709b400c4bc"},
+    {file = "hiredis-2.1.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ec060d6db9576f6723b5290448aea67160608556b5506eb947997d9d1ca6f7b7"},
+    {file = "hiredis-2.1.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8781f5b91d75abef529a33cf3509ba5fe540d2814de0c4602f0f5ba6f1669739"},
+    {file = "hiredis-2.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bd6b934794bea92a15b10ac35889df63b28d2abf9d020a7c87c05dd9c6e1edd"},
+    {file = "hiredis-2.1.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf6d85c1ffb4ec4a859b2f31cd8845e633f91ed971a3cce6f59a722dcc361b8c"},
+    {file = "hiredis-2.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:bbf80c686e3f63d40b0ab42d3605d3b6d415c368a5d8a9764a314ebda6138650"},
+    {file = "hiredis-2.1.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c1d85dfdf37a8df0e0174fc0c762b485b80a2fc7ce9592ae109aaf4a5d45ba9a"},
+    {file = "hiredis-2.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:816b9ea96e7cc2496a1ac9c4a76db670827c1e31045cc377c66e64a20bb4b3ff"},
+    {file = "hiredis-2.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db59afa0edf194bea782e4686bfc496fc1cea2e24f310d769641e343d14cc929"},
+    {file = "hiredis-2.1.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c7a7e4ccec7164cdf2a9bbedc0e7430492eb56d9355a41377f40058c481bccc"},
+    {file = "hiredis-2.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:646f150fa73f9cbc69419e34a1aae318c9f39bd9640760aa46624b2815da0c2d"},
+    {file = "hiredis-2.1.1.tar.gz", hash = "sha256:21751e4b7737aaf7261a068758b22f7670155099592b28d8dde340bf6874313d"},
 ]
 
 [[package]]
@@ -545,6 +550,36 @@ files = [
     {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
 ]
 
+[[package]]
+name = "maturin"
+version = "0.14.10"
+description = "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "maturin-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:ec8269c02cc435893308dfd50f57f14fb1be3554e4e61c5bf49b97363b289775"},
+    {file = "maturin-0.14.10-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:e9c19dc0a28109280f7d091ca7b78e25f3fc340fcfac92801829a21198fa20eb"},
+    {file = "maturin-0.14.10-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:cf950ebfe449a97617b91d75e09766509e21a389ce3f7b6ef15130ad8a95430a"},
+    {file = "maturin-0.14.10-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:c0d25e82cb6e5de9f1c028fcf069784be4165b083e79412371edce05010b68f3"},
+    {file = "maturin-0.14.10-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:9da98bee0a548ecaaa924cc8cb94e49075d5e71511c62a1633a6962c7831a29b"},
+    {file = "maturin-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2f097a63f3bed20a7da56fc7ce4d44ef8376ee9870604da16b685f2d02c87c79"},
+    {file = "maturin-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:4946ad7545ba5fc0ad08bc98bc8e9f6ffabb6ded71db9ed282ad4596b998d42a"},
+    {file = "maturin-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:98bfed21c3498857b3381efeb041d77e004a93b22261bf9690fe2b9fbb4c210f"},
+    {file = "maturin-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b157e2e8a0216d02df1d0451201fcb977baf0dcd223890abfbfbfd01e0b44630"},
+    {file = "maturin-0.14.10-py3-none-win32.whl", hash = "sha256:5abf311d4618b673efa30cacdac5ae2d462e49da58db9a5bf0d8bde16d9c16be"},
+    {file = "maturin-0.14.10-py3-none-win_amd64.whl", hash = "sha256:11b8550ceba5b81465a18d06f0d3a4cfc1cd6cbf68eda117c253bbf3324b1264"},
+    {file = "maturin-0.14.10-py3-none-win_arm64.whl", hash = "sha256:6cc9afb89f28bd591b62f8f3c29736c81c322cffe88f9ab8eb1749377bbc3521"},
+    {file = "maturin-0.14.10.tar.gz", hash = "sha256:895c48cbe56ae994c2a1eeeef19475ca4819aa4c6412af727a63a772e8ef2d87"},
+]
+
+[package.dependencies]
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+patchelf = ["patchelf"]
+zig = ["ziglang (>=0.10.0,<0.11.0)"]
+
 [[package]]
 name = "msgspec"
 version = "0.12.0"
@@ -670,14 +705,14 @@ files = [
 
 [[package]]
 name = "packaging"
-version = "22.0"
+version = "23.0"
 description = "Core utilities for Python packages"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"},
-    {file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"},
+    {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"},
+    {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
 ]
 
 [[package]]
@@ -735,14 +770,14 @@ email = ["email-validator (>=1.0.3)"]
 
 [[package]]
 name = "pydantic-factories"
-version = "1.17.0"
+version = "1.17.1"
 description = "Mock data generation for pydantic based models and python dataclasses"
 category = "main"
 optional = false
 python-versions = ">=3.8,<4.0"
 files = [
-    {file = "pydantic_factories-1.17.0-py3-none-any.whl", hash = "sha256:11c267ccbb7d54b1b456f68060f31472a5e53151314679e6f9367962b4d3a6f8"},
-    {file = "pydantic_factories-1.17.0.tar.gz", hash = "sha256:f6b2cdf715f8c3e4a84e2851c0fb493af2009398159594c000192a38ce192129"},
+    {file = "pydantic_factories-1.17.1-py3-none-any.whl", hash = "sha256:05c0b143540f54d9dd9d0d500b7b146ff29e0c1cd4bb5f2ed99c60842ff1d5e6"},
+    {file = "pydantic_factories-1.17.1.tar.gz", hash = "sha256:85848136cd768894dc5b6e3ffaf49753c7627c545ef05ff096ff616071cd59ff"},
 ]
 
 [package.dependencies]
@@ -907,14 +942,14 @@ web = ["aiohttp", "aiohttp-basicauth"]
 
 [[package]]
 name = "sentry-sdk"
-version = "1.12.1"
+version = "1.13.0"
 description = "Python client for Sentry (https://sentry.io)"
 category = "main"
 optional = false
 python-versions = "*"
 files = [
-    {file = "sentry-sdk-1.12.1.tar.gz", hash = "sha256:5bbe4b72de22f9ac1e67f2a4e6efe8fbd595bb59b7b223443f50fe5802a5551c"},
-    {file = "sentry_sdk-1.12.1-py2.py3-none-any.whl", hash = "sha256:9f0b960694e2d8bb04db4ba6ac2a645040caef4e762c65937998ff06064f10d6"},
+    {file = "sentry-sdk-1.13.0.tar.gz", hash = "sha256:72da0766c3069a3941eadbdfa0996f83f5a33e55902a19ba399557cfee1dddcc"},
+    {file = "sentry_sdk-1.13.0-py2.py3-none-any.whl", hash = "sha256:b7ff6318183e551145b5c4766eb65b59ad5b63ff234dffddc5fb50340cad6729"},
 ]
 
 [package.dependencies]
@@ -941,6 +976,7 @@ rq = ["rq (>=0.6)"]
 sanic = ["sanic (>=0.8)"]
 sqlalchemy = ["sqlalchemy (>=1.2)"]
 starlette = ["starlette (>=0.19.1)"]
+starlite = ["starlite (>=1.48)"]
 tornado = ["tornado (>=5)"]
 
 [[package]]
@@ -1047,14 +1083,14 @@ sqlcipher = ["sqlcipher3-binary"]
 
 [[package]]
 name = "starlite"
-version = "1.48.1"
+version = "1.49.0"
 description = "Performant, light and flexible ASGI API Framework"
 category = "main"
 optional = false
 python-versions = ">=3.8,<4.0"
 files = [
-    {file = "starlite-1.48.1-py3-none-any.whl", hash = "sha256:d2a37240b54199f1fa7ba2d3d2079124ee494f9f81722eec2cf04cccf4abfb8b"},
-    {file = "starlite-1.48.1.tar.gz", hash = "sha256:2a79e219d8f6498f2b9cad284615b58186c2043010dee70cefb62b930528efed"},
+    {file = "starlite-1.49.0-py3-none-any.whl", hash = "sha256:97748c7915706f5888f6eb245a4305f7c50f3fdb5bc09785421ee5677513cee9"},
+    {file = "starlite-1.49.0.tar.gz", hash = "sha256:216c84745617070b766d0e759016a13fcfb467f75b54c8735a0b1e78625363bf"},
 ]
 
 [package.dependencies]
@@ -1073,15 +1109,15 @@ typing-extensions = "*"
 
 [package.extras]
 brotli = ["brotli"]
-cli = ["click (>=8.1.3)", "rich (>=12.6.0)"]
+cli = ["click", "jsbeautifier", "rich (>=13.0.0)"]
 cryptography = ["cryptography"]
-full = ["aiomcache", "brotli", "click (>=8.1.3)", "cryptography", "opentelemetry-instrumentation-asgi", "picologging", "python-jose", "redis[hiredis]", "rich (>=12.6.0)", "structlog"]
+full = ["aiomcache", "brotli", "click", "cryptography", "opentelemetry-instrumentation-asgi", "picologging", "python-jose", "redis[hiredis]", "rich (>=13.0.0)", "structlog"]
 jwt = ["cryptography", "python-jose"]
 memcached = ["aiomcache"]
 opentelemetry = ["opentelemetry-instrumentation-asgi"]
 picologging = ["picologging"]
 redis = ["redis[hiredis]"]
-standard = ["click (>=8.1.3)", "picologging", "rich (>=12.6.0)"]
+standard = ["click", "jsbeautifier", "picologging", "rich (>=13.0.0)"]
 structlog = ["structlog"]
 
 [[package]]
@@ -1117,6 +1153,18 @@ files = [
 [package.extras]
 doc = ["reno", "sphinx", "tornado (>=4.5)"]
 
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+    {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+
 [[package]]
 name = "typing-extensions"
 version = "4.4.0"
@@ -1131,14 +1179,14 @@ files = [
 
 [[package]]
 name = "urllib3"
-version = "1.26.13"
+version = "1.26.14"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
 category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
 files = [
-    {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"},
-    {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"},
+    {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"},
+    {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"},
 ]
 
 [package.extras]
@@ -1213,4 +1261,4 @@ test = ["Cython (>=0.29.32,<0.30.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "my
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.10"
-content-hash = "d9f8dc80dc0e85303fd241049e9fa12e0ea888de741065e8a2dfe215cc5d9317"
+content-hash = "02b7b504018c2c2e6633f8028b22dc996e837ba7f1b2a6ae6b879382b9c9d2d0"
diff --git a/pyproject.toml b/pyproject.toml
index 8a46ebc3..0a0d3858 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,11 +12,11 @@ ignore-words-list = "alog"
 branch = true
 omit = ["*/starlite_saqlalchemy/scripts.py", "*/starlite_saqlalchemy/lifespan.py", "tests/*"]
 relative_files = true
-source_pkgs = ["starlite_saqlalchemy"]
+source_pkgs = ["starlite_saqlalchemy", "pytest_starlite_saqlalchemy"]
 
 [tool.coverage.paths]
 source = ["src", "*/site-packages"]
-others = [".", "*/starlite_saqlalchemy"]
+others = [".", "*/starlite_saqlalchemy", "*/pytest_starlite_saqlalchemy"]
 
 [tool.coverage.report]
 show_missing = true
@@ -48,9 +48,11 @@ classifiers = [
     "Topic :: Internet :: WWW/HTTP",
     "Topic :: Software Development",
     "Topic :: System :: Installation/Setup",
+    "Framework :: Pytest",
 ]
 packages = [
-    { include = "starlite_saqlalchemy", from = "src" }
+    { include = "starlite_saqlalchemy", from = "src" },
+    { include = "pytest_starlite_saqlalchemy", from = "src" }
 ]
 
 [tool.poetry.dependencies]
@@ -63,7 +65,7 @@ pydantic = "*"
 python-dotenv = "*"
 redis = "*"
 saq = "^0.9.1"
-sentry-sdk = "*"
+sentry-sdk = ">=1.13.0"
 sqlalchemy = "==2.0.0rc2"
 starlite = "^1.40.1"
 structlog = ">=22.2.0"
@@ -71,6 +73,12 @@ tenacity = "*"
 uvicorn = "*"
 uvloop = "*"
 
+[tool.poetry.plugins."pytest11"]
+pytest_starlite_saqlalchemy = "pytest_starlite_saqlalchemy"
+
+[tool.poetry.plugins."console_scripts"]
+run-app = "starlite_saqlalchemy.scripts:run_app"
+
 [tool.poetry.urls]
 GitHub = "https://github.com/topsport-com-au"
 Bugs = "https://github.com/topsport-com-au/starlite-saqlalchemy/issues"
@@ -88,6 +96,7 @@ addopts = ["-ra", "--strict-config"]
 asyncio_mode = "auto"
 env_files = ["tests.env"]
 testpaths = ["tests/unit"]
+test_app = "tests.utils.app:create_app"
 
 [tool.pylint.main]
 disable = [
diff --git a/requirements.ci.txt b/requirements.ci.txt
new file mode 100644
index 00000000..b90d588d
--- /dev/null
+++ b/requirements.ci.txt
@@ -0,0 +1,2 @@
+tox==4.3.1
+wheel==0.38.4
diff --git a/dev.requirements.txt b/requirements.dev.txt
similarity index 100%
rename from dev.requirements.txt
rename to requirements.dev.txt
diff --git a/docs.requirements.txt b/requirements.docs.txt
similarity index 100%
rename from docs.requirements.txt
rename to requirements.docs.txt
diff --git a/src/starlite_saqlalchemy/__init__.py b/src/starlite_saqlalchemy/__init__.py
index 044ce55f..53496bfe 100644
--- a/src/starlite_saqlalchemy/__init__.py
+++ b/src/starlite_saqlalchemy/__init__.py
@@ -29,6 +29,7 @@ def example_handler() -> dict:
     dto,
     exceptions,
     health,
+    http,
     log,
     openapi,
     redis,
@@ -51,6 +52,7 @@ def example_handler() -> dict:
     "dto",
     "exceptions",
     "health",
+    "http",
     "log",
     "openapi",
     "redis",
diff --git a/src/starlite_saqlalchemy/constants.py b/src/starlite_saqlalchemy/constants.py
new file mode 100644
index 00000000..8a5e75d3
--- /dev/null
+++ b/src/starlite_saqlalchemy/constants.py
@@ -0,0 +1,11 @@
+"""Application constants."""
+from __future__ import annotations
+
+from starlite_saqlalchemy.settings import app
+from starlite_saqlalchemy.utils import case_insensitive_string_compare
+
+IS_TEST_ENVIRONMENT = case_insensitive_string_compare(app.ENVIRONMENT, app.TEST_ENVIRONMENT_NAME)
+"""Flag indicating if the application is running in a test environment."""
+
+IS_LOCAL_ENVIRONMENT = case_insensitive_string_compare(app.ENVIRONMENT, app.LOCAL_ENVIRONMENT_NAME)
+"""Flag indicating if application is running in local development mode."""
diff --git a/src/starlite_saqlalchemy/init_plugin.py b/src/starlite_saqlalchemy/init_plugin.py
index b4171c5d..08d4a244 100644
--- a/src/starlite_saqlalchemy/init_plugin.py
+++ b/src/starlite_saqlalchemy/init_plugin.py
@@ -51,6 +51,7 @@ def example_handler() -> dict:
     settings,
     sqlalchemy_plugin,
 )
+from starlite_saqlalchemy.constants import IS_LOCAL_ENVIRONMENT, IS_TEST_ENVIRONMENT
 from starlite_saqlalchemy.exceptions import HealthCheckConfigurationError
 from starlite_saqlalchemy.health import (
     AbstractHealthCheck,
@@ -132,7 +133,7 @@ class PluginConfig(BaseModel):
     Set the OpenAPI config object to
     [`AppConfig.openapi_config`][starlite.config.app.AppConfig.openapi_config].
     """
-    do_sentry: bool = True
+    do_sentry: bool | None = None
     """Configure sentry.
 
     Configure the application to initialize Sentry on startup. Adds a handler to
@@ -330,7 +331,12 @@ def configure_sentry(self, app_config: AppConfig) -> None:
         Args:
             app_config: The Starlite application config object.
         """
-        if self.config.do_sentry:
+        do_sentry = (
+            self.config.do_sentry
+            if self.config.do_sentry is not None
+            else not (IS_LOCAL_ENVIRONMENT or IS_TEST_ENVIRONMENT)
+        )
+        if do_sentry:
             app_config.on_startup.append(sentry.configure)
 
     def configure_sqlalchemy_plugin(self, app_config: AppConfig) -> None:
diff --git a/src/starlite_saqlalchemy/log/__init__.py b/src/starlite_saqlalchemy/log/__init__.py
index 680aedb6..06105afa 100644
--- a/src/starlite_saqlalchemy/log/__init__.py
+++ b/src/starlite_saqlalchemy/log/__init__.py
@@ -9,6 +9,7 @@
 from starlite.config.logging import LoggingConfig
 
 from starlite_saqlalchemy import settings
+from starlite_saqlalchemy.constants import IS_LOCAL_ENVIRONMENT
 
 from . import controller, worker
 from .utils import EventFilter, msgspec_json_renderer
@@ -43,7 +44,7 @@
     structlog.stdlib.ProcessorFormatter.remove_processors_meta,
 ]
 
-if settings.app.ENVIRONMENT == "local":  # pragma: no cover
+if IS_LOCAL_ENVIRONMENT:  # pragma: no cover
     LoggerFactory: Any = structlog.WriteLoggerFactory
     console_processor = structlog.dev.ConsoleRenderer(colors=True)
     default_processors.extend([console_processor])
diff --git a/src/starlite_saqlalchemy/scripts.py b/src/starlite_saqlalchemy/scripts.py
index 076c8d27..326e084b 100644
--- a/src/starlite_saqlalchemy/scripts.py
+++ b/src/starlite_saqlalchemy/scripts.py
@@ -2,15 +2,12 @@
 import uvicorn
 
 from starlite_saqlalchemy import settings
+from starlite_saqlalchemy.constants import IS_LOCAL_ENVIRONMENT
 
 
 def determine_should_reload() -> bool:
     """Evaluate whether reloading should be enabled."""
-    return (
-        settings.server.RELOAD
-        if settings.server.RELOAD is not None
-        else settings.app.ENVIRONMENT == "local"
-    )
+    return settings.server.RELOAD if settings.server.RELOAD is not None else IS_LOCAL_ENVIRONMENT
 
 
 def determine_reload_dirs(should_reload: bool) -> list[str] | None:
diff --git a/src/starlite_saqlalchemy/sentry.py b/src/starlite_saqlalchemy/sentry.py
index 9a2a0c5a..d968fc63 100644
--- a/src/starlite_saqlalchemy/sentry.py
+++ b/src/starlite_saqlalchemy/sentry.py
@@ -1,26 +1,46 @@
-"""Sentry config for our application.
-
-The current support for sentry is limited, but still worth having.
-
-See: https://github.com/getsentry/sentry-python/issues/1549
-"""
+"""Sentry config for our application."""
 from __future__ import annotations
 
+from typing import TYPE_CHECKING, TypedDict, cast
+
 import sentry_sdk
 from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
+from sentry_sdk.integrations.starlite import StarliteIntegration
 
 from starlite_saqlalchemy import settings
 
+if TYPE_CHECKING:
+    from collections.abc import Mapping
+    from typing import Any
+
+    from starlite.types import HTTPScope
+
+
+class SamplingContext(TypedDict):
+    """Sentry context sent to traces sampler function."""
+
+    asgi_scope: HTTPScope
+    parent_sampled: bool | None
+    transaction_context: dict[str, Any]
+
+
+def sentry_traces_sampler(sampling_context: Mapping[str, Any]) -> float:
+    """Don't send health check transactions to sentry."""
+    sampling_context = cast("SamplingContext", sampling_context)
+    if sampling_context["asgi_scope"]["path"] == settings.api.HEALTH_PATH:
+        return 0.0
+    return settings.sentry.TRACES_SAMPLE_RATE
+
 
 def configure() -> None:
     """Configure sentry on app startup.
 
     See [SentrySettings][starlite_saqlalchemy.settings.SentrySettings].
     """
-    sentry_sdk.init(
+    sentry_sdk.init(  # pragma: no cover
         dsn=settings.sentry.DSN,
         environment=settings.app.ENVIRONMENT,
         release=settings.app.BUILD_NUMBER,
-        integrations=[SqlalchemyIntegration()],
-        traces_sample_rate=settings.sentry.TRACES_SAMPLE_RATE,
+        integrations=[StarliteIntegration(), SqlalchemyIntegration()],
+        traces_sampler=sentry_traces_sampler,
     )
diff --git a/src/starlite_saqlalchemy/settings.py b/src/starlite_saqlalchemy/settings.py
index f14f4630..b182a6d8 100644
--- a/src/starlite_saqlalchemy/settings.py
+++ b/src/starlite_saqlalchemy/settings.py
@@ -35,6 +35,17 @@ class Config:
     """Run `Starlite` with `debug=True`."""
     ENVIRONMENT: str = "prod"
     """'dev', 'prod', etc."""
+    TEST_ENVIRONMENT_NAME: str = "test"
+    """Value of ENVIRONMENT used to determine if running tests.
+
+    This should be the value of `ENVIRONMENT` in `test.env`.
+    """
+    LOCAL_ENVIRONMENT_NAME: str = "local"
+    """Value of ENVIRONMENT used to determine if running in local development
+    mode.
+
+    This should be the value of `ENVIRONMENT` in your local `.env` file.
+    """
     NAME: str = "my-starlite-saqlalchemy-app"
     """Application name."""
 
@@ -113,7 +124,6 @@ class Config:
     REQUEST_FIELDS: list[RequestExtractorField] = [
         "path",
         "method",
-        "content_type",
         "headers",
         "cookies",
         "query",
diff --git a/src/starlite_saqlalchemy/sqlalchemy_plugin.py b/src/starlite_saqlalchemy/sqlalchemy_plugin.py
index 821b26b8..40626a74 100644
--- a/src/starlite_saqlalchemy/sqlalchemy_plugin.py
+++ b/src/starlite_saqlalchemy/sqlalchemy_plugin.py
@@ -19,7 +19,7 @@
     from starlite.datastructures.state import State
     from starlite.types import Message, Scope
 
-__all__ = ["config", "plugin"]
+__all__ = ["SQLAlchemyHealthCheck", "config", "plugin"]
 
 
 async def before_send_handler(message: "Message", _: "State", scope: "Scope") -> None:
@@ -49,6 +49,7 @@ class SQLAlchemyHealthCheck(AbstractHealthCheck):
     name: str = "db"
 
     def __init__(self) -> None:
+        """Health check with database check."""
         self.engine = create_async_engine(
             settings.db.URL, logging_name="starlite_saqlalchemy.health"
         )
diff --git a/src/starlite_saqlalchemy/utils.py b/src/starlite_saqlalchemy/utils.py
index c140ac36..d949f2dc 100644
--- a/src/starlite_saqlalchemy/utils.py
+++ b/src/starlite_saqlalchemy/utils.py
@@ -3,6 +3,11 @@
 from typing import Any
 
 
+def case_insensitive_string_compare(a: str, b: str, /) -> bool:
+    """Compare `a` and `b`, stripping whitespace and ignoring case."""
+    return a.strip().lower() == b.strip().lower()
+
+
 def dataclass_as_dict_shallow(dataclass: Any, *, exclude_none: bool = False) -> dict[str, Any]:
     """Convert a dataclass to dict, without deepcopy."""
     ret: dict[str, Any] = {}
diff --git a/tox.ini b/tox.ini
index a70b4aa5..93c842a5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,21 +1,25 @@
 [gh-actions]
 python =
     3.10: py310
-    3.11: py311,integration
+    3.11: py311,pytest-plugin,integration
 
 [tox]
-envlist = pylint,mypy,pyright,py310,py311,integration,coverage
+envlist = pylint,mypy,pyright,py310,py311,pytest-plugin,integration,coverage
 isolated_build = true
 
 [testenv]
 deps =
-  -r{toxinidir}/dev.requirements.txt
+  -r{toxinidir}/requirements.dev.txt
 
 commands =
     coverage run -p -m pytest {posargs}
 
+[testenv:pytest-plugin]
+basepython = python3.11
+commands = coverage run -p -m pytest tests/pytest_plugin {posargs}
+
 [testenv:coverage]
-depends = py310,py311
+depends = py310,py311,pytest-plugin
 basepython = python3.11
 commands =
     coverage combine
@@ -74,6 +78,6 @@ basepython = python3.11
 passenv =
     HOME
 deps =
-    -r{toxinidir}/docs.requirements.txt
+    -r{toxinidir}/requirements.docs.txt
 commands =
     mike {posargs: serve}


Test patch:
diff --git a/docs/testing/pytest_plugin.md b/docs/testing/pytest_plugin.md
new file mode 100644
index 00000000..bf6bf6d1
--- /dev/null
+++ b/docs/testing/pytest_plugin.md
@@ -0,0 +1,137 @@
+# Pytest Plugin
+
+The nature of applications built with the `starlite-saqlalchemy` pattern is that they rely heavily
+on connected services.
+
+Abstraction of [PostgreSQL][2] and [Redis][3] connectivity boilerplate is a nice convenience,
+however to successfully patch the application for testing requires deeper knowledge of the
+implementation than would be otherwise necessary.
+
+So, `starlite-saqlalchemy` ships with a selection of [pytest fixtures][1] that are often necessary
+when building applications such as these.
+
+## `app`
+
+The `app` fixture provides an instance of a `Starlite` application.
+
+```python
+from __future__ import annotations
+
+from starlite import Starlite
+
+
+def test_app_fixture(app: Starlite) -> None:
+    assert isinstance(app, Starlite)
+```
+
+The value of Pytest ini option, `test_app` is used to determine the application to load.
+
+```toml
+# pyproject.toml
+
+[tool.pytest.ini_options]
+test_app = "app.main:create_app"
+```
+
+If no value is configured for the `test_app` ini option, the default location of
+`"app.main:create_app"` is searched.
+
+The value of the `test_app` ini option can either point to an application factory or `Starlite`
+instance.
+
+If the object found at the import path is not a `Starlite` instance, the fixture assumes it is
+an application factory, and will call the object and return the response.
+
+The value of `test_app` is resolved using the uvicorn `import_from_string()` function, so it
+supports the same format as `uvicorn` supports for its `app` and `factory` parameters.
+
+## `client`
+
+A `starlite.testing.TestClient` instance, wired to the same application that is produced by the
+`app` fixture.
+
+## `cap_logger`
+
+The `cap_logger` fixture provides an instance of [`structlog.testing.CapturingLogger`][4].
+
+```python
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from structlog.testing import CapturedCall
+
+if TYPE_CHECKING:
+    from structlog.testing import CapturingLogger
+
+
+def test_app_fixture(cap_logger: CapturingLogger) -> None:
+    cap_logger.info("hello")
+    cap_logger.info("hello", when="again")
+    assert cap_logger.calls == [
+        CapturedCall(method_name="info", args=("hello",), kwargs={}),
+        CapturedCall(method_name="info", args=("hello",), kwargs={"when": "again"}),
+    ]
+```
+
+The `cap_logger` fixture will capture any `structlog` calls made by the starlite application or the
+SAQ worker, so that they can be inspected as part of tests.
+
+```python
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from httpx import AsyncClient
+
+if TYPE_CHECKING:
+    from starlite import Starlite
+    from structlog.testing import CapturingLogger
+
+
+async def test_health_logging_skipped(
+    app: Starlite, cap_logger: CapturingLogger
+) -> None:
+    """Test that calls to the health check route are not logged."""
+
+    async with AsyncClient(app=app, base_url="http://testserver") as client:
+        response = await client.get("/health")
+        assert response.status_code == 200
+
+    assert [] == cap_logger.calls
+```
+
+## is_unit_test
+
+The `is_unit_test` fixture returns a `bool` that indicates if the test suite believes it is running
+a unit test, or an integration test.
+
+To determine this, we compare the path of the running test to the value of the Pytest ini option
+`unit_test_pattern`, which by default is `"^.*/tests/unit/.*$"`.
+
+This fixture is used to make fixtures behave differently between unit and integration test contexts.
+
+## _patch_http_close
+
+This is an [`autouse` fixture][5], that prevents HTTP clients that are defined in the global scope
+from being closed.
+
+The application is configured to close all instantiated HTTP clients on app shutdown, however when
+apps are defined in a global/class scope, a test that runs after the first application shutdown in
+the test suite would fail.
+
+## _patch_sqlalchemy_plugin
+
+This is an [`autouse` fixture][5], that mocks out the `on_shutdown` method of the SQLAlchemy config
+object for unit tests.
+
+## _patch_worker
+
+This is an [`autouse` fixture][5], that mocks out the `on_app_startup` and `stop` methods of
+`worker.Worker` type for unit tests.
+
+[1]: https://docs.pytest.org/en/latest/explanation/fixtures.html#about-fixtures
+[2]: https://www.postgresql.org/
+[3]: https://redis.io
+[4]: https://www.structlog.org/en/stable/api.html#structlog.testing.CapturingLogger
+[5]: https://docs.pytest.org/en/6.2.x/fixture.html#autouse-fixtures-fixtures-you-don-t-have-to-request
diff --git a/src/pytest_starlite_saqlalchemy/__init__.py b/src/pytest_starlite_saqlalchemy/__init__.py
new file mode 100644
index 00000000..f89939e8
--- /dev/null
+++ b/src/pytest_starlite_saqlalchemy/__init__.py
@@ -0,0 +1,13 @@
+"""Pytest plugin to support testing starlite-saqlalchemy applications."""
+from __future__ import annotations
+
+from .plugin import (
+    _patch_http_close,
+    _patch_sqlalchemy_plugin,
+    _patch_worker,
+    fx_app,
+    fx_cap_logger,
+    fx_client,
+    fx_is_unit_test,
+    pytest_addoption,
+)
diff --git a/src/pytest_starlite_saqlalchemy/plugin.py b/src/pytest_starlite_saqlalchemy/plugin.py
new file mode 100644
index 00000000..048351c2
--- /dev/null
+++ b/src/pytest_starlite_saqlalchemy/plugin.py
@@ -0,0 +1,139 @@
+"""Pytest plugin to support testing starlite-saqlalchemy applications."""
+# pylint: disable=import-outside-toplevel
+from __future__ import annotations
+
+import re
+from typing import TYPE_CHECKING
+from unittest.mock import MagicMock
+
+import pytest
+from starlite import Starlite, TestClient
+from structlog.contextvars import clear_contextvars
+from structlog.testing import CapturingLogger
+from uvicorn.importer import ImportFromStringError, import_from_string
+
+if TYPE_CHECKING:
+    from collections.abc import Generator
+
+    from pytest import Config, FixtureRequest, MonkeyPatch, Parser
+
+__all__ = (
+    "_patch_http_close",
+    "_patch_sqlalchemy_plugin",
+    "_patch_worker",
+    "fx_app",
+    "fx_cap_logger",
+    "fx_client",
+    "fx_is_unit_test",
+    "pytest_addoption",
+)
+
+
+def pytest_addoption(parser: Parser) -> None:
+    """Adds Pytest ini config variables for the plugin."""
+    parser.addini(
+        "test_app",
+        "Path to application instance, or callable that returns an application instance.",
+        type="string",
+        default="app.main:create_app",
+    )
+    parser.addini(
+        "unit_test_pattern",
+        (
+            "Regex used to identify if a test is running as part of a unit or integration test "
+            "suite. The pattern is matched against the path of each test function and affects the "
+            "behavior of fixtures that are shared between unit and integration tests."
+        ),
+        type="string",
+        default=r"^.*/tests/unit/.*$",
+    )
+
+
+@pytest.fixture(name="is_unit_test")
+def fx_is_unit_test(request: FixtureRequest) -> bool:
+    """Uses the ini option `unit_test_pattern` to determine if the test is part
+    of unit or integration tests."""
+    unittest_pattern: str = request.config.getini("unit_test_pattern")  # pyright:ignore
+    return bool(re.search(unittest_pattern, str(request.path)))
+
+
+@pytest.fixture(autouse=True)
+def _patch_http_close(monkeypatch: MonkeyPatch) -> None:
+    """We don't want global http clients to get closed between tests."""
+    import starlite_saqlalchemy
+
+    monkeypatch.setattr(starlite_saqlalchemy.http, "clients", set())
+
+
+@pytest.fixture(autouse=True)
+def _patch_sqlalchemy_plugin(is_unit_test: bool, monkeypatch: MonkeyPatch) -> None:
+    if is_unit_test:
+        from starlite_saqlalchemy import sqlalchemy_plugin
+
+        monkeypatch.setattr(
+            sqlalchemy_plugin.SQLAlchemyConfig,  # type:ignore[attr-defined]
+            "on_shutdown",
+            MagicMock(),
+        )
+
+
+@pytest.fixture(autouse=True)
+def _patch_worker(is_unit_test: bool, monkeypatch: MonkeyPatch) -> None:
+    """We don't want the worker to start for unittests."""
+    if is_unit_test:
+        from starlite_saqlalchemy import worker
+
+        monkeypatch.setattr(worker.Worker, "on_app_startup", MagicMock())
+        monkeypatch.setattr(worker.Worker, "stop", MagicMock())
+
+
+@pytest.fixture(name="app")
+def fx_app(pytestconfig: Config, monkeypatch: MonkeyPatch) -> Starlite:
+    """
+    Returns:
+        An application instance, configured via plugin.
+    """
+    test_app_str = pytestconfig.getini("test_app")
+
+    try:
+        app_or_callable = import_from_string(test_app_str)
+    except (ImportFromStringError, ModuleNotFoundError):
+        from starlite_saqlalchemy.init_plugin import ConfigureApp
+
+        app = Starlite(route_handlers=[], on_app_init=[ConfigureApp()], openapi_config=None)
+    else:
+        if isinstance(app_or_callable, Starlite):
+            app = app_or_callable
+        else:
+            app = app_or_callable()
+
+    monkeypatch.setattr(app, "before_startup", [])
+    return app
+
+
+@pytest.fixture(name="client")
+def fx_client(app: Starlite) -> Generator[TestClient, None, None]:
+    """Test client fixture for making calls on the global app instance."""
+    with TestClient(app=app) as client:
+        yield client
+
+
+@pytest.fixture(name="cap_logger")
+def fx_cap_logger(monkeypatch: MonkeyPatch) -> CapturingLogger:
+    """Used to monkeypatch the app logger, so we can inspect output."""
+    import starlite_saqlalchemy
+
+    starlite_saqlalchemy.log.configure(
+        starlite_saqlalchemy.log.default_processors  # type:ignore[arg-type]
+    )
+    # clear context for every test
+    clear_contextvars()
+    # pylint: disable=protected-access
+    logger = starlite_saqlalchemy.log.controller.LOGGER.bind()
+    logger._logger = CapturingLogger()
+    # drop rendering processor to get a dict, not bytes
+    # noinspection PyProtectedMember
+    logger._processors = starlite_saqlalchemy.log.default_processors[:-1]
+    monkeypatch.setattr(starlite_saqlalchemy.log.controller, "LOGGER", logger)
+    monkeypatch.setattr(starlite_saqlalchemy.log.worker, "LOGGER", logger)
+    return logger._logger
diff --git a/src/starlite_saqlalchemy/testing/__init__.py b/src/starlite_saqlalchemy/testing/__init__.py
new file mode 100644
index 00000000..8d75d555
--- /dev/null
+++ b/src/starlite_saqlalchemy/testing/__init__.py
@@ -0,0 +1,11 @@
+"""Application testing support."""
+
+from .controller_test import ControllerTest
+from .generic_mock_repository import GenericMockRepository
+from .modify_settings import modify_settings
+
+__all__ = (
+    "ControllerTest",
+    "GenericMockRepository",
+    "modify_settings",
+)
diff --git a/src/starlite_saqlalchemy/testing/controller_test.py b/src/starlite_saqlalchemy/testing/controller_test.py
new file mode 100644
index 00000000..78ae14f6
--- /dev/null
+++ b/src/starlite_saqlalchemy/testing/controller_test.py
@@ -0,0 +1,119 @@
+"""Automated controller testing."""
+from __future__ import annotations
+
+import random
+from typing import TYPE_CHECKING
+
+from starlite.status_codes import (
+    HTTP_200_OK,
+    HTTP_201_CREATED,
+    HTTP_405_METHOD_NOT_ALLOWED,
+)
+
+if TYPE_CHECKING:
+    from collections.abc import Sequence
+    from typing import Any
+
+    from pytest import MonkeyPatch
+    from starlite.testing import TestClient
+
+    from starlite_saqlalchemy.db import orm
+    from starlite_saqlalchemy.service import Service
+
+
+class ControllerTest:
+    """Standard controller testing utility."""
+
+    def __init__(
+        self,
+        client: TestClient,
+        base_path: str,
+        collection: Sequence[orm.Base],
+        raw_collection: Sequence[dict[str, Any]],
+        service_type: type[Service],
+        monkeypatch: MonkeyPatch,
+        collection_filters: dict[str, Any] | None = None,
+    ) -> None:
+        """Perform standard tests of controllers.
+
+        Args:
+            client: Test client instance.
+            base_path: Path for POST and collection GET requests.
+            collection: Collection of domain objects.
+            raw_collection: Collection of raw representations of domain objects.
+            service_type: The domain Service object type.
+            monkeypatch: Pytest's monkeypatch.
+            collection_filters: Collection filters for GET collection request.
+        """
+        self.client = client
+        self.base_path = base_path
+        self.collection = collection
+        self.raw_collection = raw_collection
+        self.service_type = service_type
+        self.monkeypatch = monkeypatch
+        self.collection_filters = collection_filters
+
+    def _get_random_member(self) -> Any:
+        return random.choice(self.collection)
+
+    def _get_raw_for_member(self, member: Any) -> dict[str, Any]:
+        return [item for item in self.raw_collection if item["id"] == str(member.id)][0]
+
+    def test_get_collection(self, with_filters: bool = False) -> None:
+        """Test collection endpoint get request."""
+
+        async def _list(*_: Any, **__: Any) -> list[Any]:
+            return list(self.collection)
+
+        self.monkeypatch.setattr(self.service_type, "list", _list)
+
+        resp = self.client.get(
+            self.base_path, params=self.collection_filters if with_filters else None
+        )
+
+        if resp.status_code == HTTP_405_METHOD_NOT_ALLOWED:
+            return
+
+        assert resp.status_code == HTTP_200_OK
+        assert resp.json() == self.raw_collection
+
+    def test_member_request(self, method: str, service_method: str, exp_status: int) -> None:
+        """Test member endpoint request."""
+        member = self._get_random_member()
+        raw = self._get_raw_for_member(member)
+
+        async def _method(*_: Any, **__: Any) -> Any:
+            return member
+
+        self.monkeypatch.setattr(self.service_type, service_method, _method)
+
+        if method.lower() == "post":
+            url = self.base_path
+        else:
+            url = f"{self.base_path}/{member.id}"
+
+        request_kw: dict[str, Any] = {}
+        if method.lower() in ("put", "post"):
+            request_kw["json"] = raw
+
+        resp = self.client.request(method, url, **request_kw)
+
+        if resp.status_code == HTTP_405_METHOD_NOT_ALLOWED:
+            return
+
+        assert resp.status_code == exp_status
+        assert resp.json() == raw
+
+    def run(self) -> None:
+        """Run the tests."""
+        # test the collection route with and without filters for branch coverage.
+        self.test_get_collection()
+        if self.collection_filters:
+            self.test_get_collection(with_filters=True)
+        for method, service_method, status in [
+            ("GET", "get", HTTP_200_OK),
+            ("PUT", "update", HTTP_200_OK),
+            ("POST", "create", HTTP_201_CREATED),
+            ("DELETE", "delete", HTTP_200_OK),
+        ]:
+            self.test_member_request(method, service_method, status)
diff --git a/src/starlite_saqlalchemy/testing.py b/src/starlite_saqlalchemy/testing/generic_mock_repository.py
similarity index 56%
rename from src/starlite_saqlalchemy/testing.py
rename to src/starlite_saqlalchemy/testing/generic_mock_repository.py
index 3f341641..9342b5ea 100644
--- a/src/starlite_saqlalchemy/testing.py
+++ b/src/starlite_saqlalchemy/testing/generic_mock_repository.py
@@ -4,66 +4,24 @@
 """
 from __future__ import annotations
 
-import random
-from contextlib import contextmanager
 from datetime import datetime
 from typing import TYPE_CHECKING, Generic, TypeVar
 from uuid import uuid4
 
-from starlite.status_codes import HTTP_200_OK, HTTP_201_CREATED
-
 from starlite_saqlalchemy.db import orm
 from starlite_saqlalchemy.exceptions import ConflictError, StarliteSaqlalchemyError
 from starlite_saqlalchemy.repository.abc import AbstractRepository
 
 if TYPE_CHECKING:
-    from collections.abc import (
-        Callable,
-        Generator,
-        Hashable,
-        Iterable,
-        MutableMapping,
-        Sequence,
-    )
+    from collections.abc import Callable, Hashable, Iterable, MutableMapping
     from typing import Any
 
-    from pydantic import BaseSettings
-    from pytest import MonkeyPatch
-    from starlite.testing import TestClient
-
     from starlite_saqlalchemy.repository.types import FilterTypes
-    from starlite_saqlalchemy.service import Service
 
 ModelT = TypeVar("ModelT", bound=orm.Base)
 MockRepoT = TypeVar("MockRepoT", bound="GenericMockRepository")
 
 
-@contextmanager
-def modify_settings(*update: tuple[BaseSettings, dict[str, Any]]) -> Generator[None, None, None]:
-    """Context manager that modify the desired settings and restore them on
-    exit.
-
-    >>> assert settings.app.ENVIRONMENT = "local"
-    >>> with modify_settings((settings.app, {"ENVIRONMENT": "prod"})):
-    >>>     assert settings.app.ENVIRONMENT == "prod"
-    >>> assert settings.app.ENVIRONMENT == "local"
-    """
-    old_settings: list[tuple[BaseSettings, dict[str, Any]]] = []
-    try:
-        for model, new_values in update:
-            old_values = {}
-            for field, value in model.dict().items():
-                if field in new_values:
-                    old_values[field] = value
-                    setattr(model, field, new_values[field])
-            old_settings.append((model, old_values))
-        yield
-    finally:
-        for model, old_values in old_settings:
-            for field, old_val in old_values.items():
-                setattr(model, field, old_val)
-
-
 class GenericMockRepository(AbstractRepository[ModelT], Generic[ModelT]):
     """A repository implementation for tests.
 
@@ -228,95 +186,3 @@ def seed_collection(cls, instances: Iterable[ModelT]) -> None:
     def clear_collection(cls) -> None:
         """Empty the collection for repository type."""
         cls.collection = {}
-
-
-class ControllerTest:
-    """Standard controller testing utility."""
-
-    def __init__(
-        self,
-        client: TestClient,
-        base_path: str,
-        collection: Sequence[orm.Base],
-        raw_collection: Sequence[dict[str, Any]],
-        service_type: type[Service],
-        monkeypatch: MonkeyPatch,
-        collection_filters: dict[str, Any] | None = None,
-    ) -> None:
-        """Perform standard tests of controllers.
-
-        Args:
-            client: Test client instance.
-            base_path: Path for POST and collection GET requests.
-            collection: Collection of domain objects.
-            raw_collection: Collection of raw representations of domain objects.
-            service_type: The domain Service object type.
-            monkeypatch: Pytest's monkeypatch.
-            collection_filters: Collection filters for GET collection request.
-        """
-        self.client = client
-        self.base_path = base_path
-        self.collection = collection
-        self.raw_collection = raw_collection
-        self.service_type = service_type
-        self.monkeypatch = monkeypatch
-        self.collection_filters = collection_filters
-
-    def _get_random_member(self) -> Any:
-        return random.choice(self.collection)
-
-    def _get_raw_for_member(self, member: Any) -> dict[str, Any]:
-        return [item for item in self.raw_collection if item["id"] == str(member.id)][0]
-
-    def test_get_collection(self, with_filters: bool = False) -> None:
-        """Test collection endpoint get request."""
-
-        async def _list(*_: Any, **__: Any) -> list[Any]:
-            return list(self.collection)
-
-        self.monkeypatch.setattr(self.service_type, "list", _list)
-
-        resp = self.client.get(
-            self.base_path, params=self.collection_filters if with_filters else None
-        )
-
-        assert resp.status_code == HTTP_200_OK
-        assert resp.json() == self.raw_collection
-
-    def test_member_request(self, method: str, service_method: str, exp_status: int) -> None:
-        """Test member endpoint request."""
-        member = self._get_random_member()
-        raw = self._get_raw_for_member(member)
-
-        async def _method(*_: Any, **__: Any) -> Any:
-            return member
-
-        self.monkeypatch.setattr(self.service_type, service_method, _method)
-
-        if method.lower() == "post":
-            url = self.base_path
-        else:
-            url = f"{self.base_path}/{member.id}"
-
-        request_kw: dict[str, Any] = {}
-        if method.lower() in ("put", "post"):
-            request_kw["json"] = raw
-
-        resp = self.client.request(method, url, **request_kw)
-
-        assert resp.status_code == exp_status
-        assert resp.json() == raw
-
-    def run(self) -> None:
-        """Run the tests."""
-        # test the collection route with and without filters for branch coverage.
-        self.test_get_collection()
-        if self.collection_filters:
-            self.test_get_collection(with_filters=True)
-        for method, service_method, status in [
-            ("GET", "get", HTTP_200_OK),
-            ("PUT", "update", HTTP_200_OK),
-            ("POST", "create", HTTP_201_CREATED),
-            ("DELETE", "delete", HTTP_200_OK),
-        ]:
-            self.test_member_request(method, service_method, status)
diff --git a/src/starlite_saqlalchemy/testing/modify_settings.py b/src/starlite_saqlalchemy/testing/modify_settings.py
new file mode 100644
index 00000000..95c4b8be
--- /dev/null
+++ b/src/starlite_saqlalchemy/testing/modify_settings.py
@@ -0,0 +1,37 @@
+"""A context manager to support patching application settings."""
+from __future__ import annotations
+
+from contextlib import contextmanager
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from collections.abc import Generator
+    from typing import Any
+
+    from pydantic import BaseSettings
+
+
+@contextmanager
+def modify_settings(*update: tuple[BaseSettings, dict[str, Any]]) -> Generator[None, None, None]:
+    """Context manager that modify the desired settings and restore them on
+    exit.
+
+    >>> assert settings.app.ENVIRONMENT = "local"
+    >>> with modify_settings((settings.app, {"ENVIRONMENT": "prod"})):
+    >>>     assert settings.app.ENVIRONMENT == "prod"
+    >>> assert settings.app.ENVIRONMENT == "local"
+    """
+    old_settings: list[tuple[BaseSettings, dict[str, Any]]] = []
+    try:
+        for model, new_values in update:
+            old_values = {}
+            for field, value in model.dict().items():
+                if field in new_values:
+                    old_values[field] = value
+                    setattr(model, field, new_values[field])
+            old_settings.append((model, old_values))
+        yield
+    finally:
+        for model, old_values in old_settings:
+            for field, old_val in old_values.items():
+                setattr(model, field, old_val)
diff --git a/tests.env b/tests.env
index 774c73dd..e5a31403 100644
--- a/tests.env
+++ b/tests.env
@@ -1,43 +1,3 @@
 # App
-BUILD_NUMBER=
-DEBUG=true
 ENVIRONMENT=test
 NAME=my-starlite-app
-
-# API
-API_CACHE_EXPIRATION=60
-API_DEFAULT_PAGINATION_LIMIT=100
-API_HEALTH_PATH=/health
-API_DB_SESSION_DEPENDENCY_KEY=db_session
-
-# OpenAPI
-OPENAPI_CONTACT_EMAIL=some_human@email.com
-OPENAPI_CONTACT_NAME="Some Human"
-OPENAPI_TITLE="My Starlite App"
-OPENAPI_VERSION=1.0.0
-
-# Sentry
-SENTRY_DSN=
-SENTRY_TRACES_SAMPLE_RATE=0.0001
-
-# Redis
-REDIS_URL=redis://cache.local:6379/0
-
-# Database
-DB_ECHO=false
-DB_ECHO_POOL=false
-DB_POOL_DISABLE=false
-DB_POOL_MAX_OVERFLOW=10
-DB_POOL_SIZE=5
-DB_POOL_TIMEOUT=30
-DB_URL=postgresql+asyncpg://postgres:mysecretpassword@pg.db.local:5432/db
-
-# Log
-LOG_EXCLUDE_PATHS="\A(?!x)x"
-LOG_OBFUSCATE_COOKIES=["session"]
-LOG_OBFUSCATE_HEADERS=["Authorization", "X-API-KEY"]
-LOG_REQUEST_FIELDS=["path","method","content_type","headers","cookies","query","path_params","body"]
-LOG_RESPONSE_FIELDS=["status_code","cookies","headers","body"]
-LOG_HTTP_EVENT="HTTP"
-LOG_WORKER_EVENT="Worker"
-LOG_LEVEL=20
diff --git a/tests/conftest.py b/tests/conftest.py
index fa2db5ed..761a66f5 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -8,12 +8,7 @@
 
 import pytest
 from asyncpg.pgproto import pgproto
-from starlite import Starlite
-from structlog.contextvars import clear_contextvars
-from structlog.testing import CapturingLogger
 
-import starlite_saqlalchemy
-from starlite_saqlalchemy import ConfigureApp, log
 from tests.utils.domain import authors, books
 
 if TYPE_CHECKING:
@@ -25,39 +20,6 @@
     from pytest import MonkeyPatch
 
 
-@pytest.fixture(name="cap_logger")
-def fx_capturing_logger(monkeypatch: MonkeyPatch) -> CapturingLogger:
-    """Used to monkeypatch the app logger, so we can inspect output."""
-    cap_logger = CapturingLogger()
-    starlite_saqlalchemy.log.configure(
-        starlite_saqlalchemy.log.default_processors  # type:ignore[arg-type]
-    )
-    # clear context for every test
-    clear_contextvars()
-    # pylint: disable=protected-access
-    logger = starlite_saqlalchemy.log.controller.LOGGER.bind()
-    logger._logger = cap_logger
-    # drop rendering processor to get a dict, not bytes
-    # noinspection PyProtectedMember
-    logger._processors = log.default_processors[:-1]
-    monkeypatch.setattr(starlite_saqlalchemy.log.controller, "LOGGER", logger)
-    monkeypatch.setattr(starlite_saqlalchemy.log.worker, "LOGGER", logger)
-    return cap_logger
-
-
-@pytest.fixture(name="app")
-def fx_app() -> Starlite:
-    """Always use this `app` fixture and never do `from app.main import app`
-    inside a test module. We need to delay import of the `app.main` module
-    until as late as possible to ensure we can mock everything necessary before
-    the application instance is constructed.
-
-    Returns:
-        The application instance.
-    """
-    return Starlite(route_handlers=[], on_app_init=[ConfigureApp()], openapi_config=None)
-
-
 @pytest.fixture(name="raw_authors")
 def fx_raw_authors() -> list[dict[str, Any]]:
     """Unstructured author representations."""
diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py
index f960b52a..ad1d9ece 100644
--- a/tests/integration/conftest.py
+++ b/tests/integration/conftest.py
@@ -1,9 +1,9 @@
 """Config for integration tests."""
-# pylint: disable=redefined-outer-name
 from __future__ import annotations
 
 import asyncio
 import timeit
+from asyncio import AbstractEventLoop, get_event_loop_policy
 from pathlib import Path
 from typing import TYPE_CHECKING
 
@@ -16,15 +16,13 @@
 from sqlalchemy.engine import URL
 from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
 from sqlalchemy.pool import NullPool
-from starlite import Provide, Router
 
 from starlite_saqlalchemy import db, sqlalchemy_plugin, worker
 from starlite_saqlalchemy.health import AppHealthCheck, HealthController
 from starlite_saqlalchemy.sqlalchemy_plugin import SQLAlchemyHealthCheck
-from tests.utils import controllers
 
 if TYPE_CHECKING:
-    from collections import abc
+    from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
     from typing import Any
 
     from pytest_docker.plugin import Services  # type:ignore[import]
@@ -37,10 +35,10 @@
 
 
 @pytest.fixture(scope="session")
-def event_loop() -> abc.Iterator[asyncio.AbstractEventLoop]:
+def event_loop() -> Iterator[AbstractEventLoop]:
     """Need the event loop scoped to the session so that we can use it to check
     containers are ready in session scoped containers fixture."""
-    policy = asyncio.get_event_loop_policy()
+    policy = get_event_loop_policy()
     loop = policy.new_event_loop()
     yield loop
     loop.close()
@@ -56,7 +54,7 @@ def docker_compose_file() -> Path:
 
 
 async def wait_until_responsive(
-    check: abc.Callable[..., abc.Awaitable], timeout: float, pause: float, **kwargs: Any
+    check: Callable[..., Awaitable], timeout: float, pause: float, **kwargs: Any
 ) -> None:
     """Wait until a service is responsive.
 
@@ -130,8 +128,8 @@ async def _containers(
     await wait_until_responsive(timeout=30.0, pause=0.1, check=redis_responsive, host=docker_ip)
 
 
-@pytest.fixture()
-async def redis(docker_ip: str) -> Redis:
+@pytest.fixture(name="redis")
+async def fx_redis(docker_ip: str) -> Redis:
     """
 
     Args:
@@ -143,8 +141,8 @@ async def redis(docker_ip: str) -> Redis:
     return Redis(host=docker_ip, port=6397)
 
 
-@pytest.fixture()
-async def engine(docker_ip: str) -> AsyncEngine:
+@pytest.fixture(name="engine")
+async def fx_engine(docker_ip: str) -> AsyncEngine:
     """Postgresql instance for end-to-end testing.
 
     Args:
@@ -169,7 +167,7 @@ async def engine(docker_ip: str) -> AsyncEngine:
 
 
 @pytest.fixture(autouse=True)
-async def _seed_db(engine: AsyncEngine, authors: list[Author]) -> abc.AsyncIterator[None]:
+async def _seed_db(engine: AsyncEngine, authors: list[Author]) -> AsyncIterator[None]:
     """Populate test database with.
 
     Args:
@@ -207,42 +205,8 @@ def _patch_redis(app: Starlite, redis: Redis, monkeypatch: pytest.MonkeyPatch) -
     monkeypatch.setattr(worker.queue, "redis", redis)
 
 
-@pytest.fixture()
-def router() -> Router:
-    """
-    Returns:
-        This is a router with controllers added for testing against the test domain.
-    """
-    return Router(
-        path="/authors",
-        route_handlers=[
-            controllers.get_authors,
-            controllers.create_author,
-            controllers.get_author,
-            controllers.update_author,
-            controllers.delete_author,
-        ],
-        dependencies={"service": Provide(controllers.provides_service)},
-        tags=["Authors"],
-    )
-
-
-@pytest.fixture()
-def app(app: Starlite, router: Router) -> Starlite:
-    """
-    Args:
-        app: App from outermost conftest.py
-        router: Router with controllers for tests.
-
-    Returns:
-        App with router attached for integration tests.
-    """
-    app.register(router)
-    return app
-
-
 @pytest.fixture(name="client")
-async def fx_client(app: Starlite) -> abc.AsyncIterator[AsyncClient]:
+async def fx_client(app: Starlite) -> AsyncIterator[AsyncClient]:
     """Async client that calls requests on the app.
 
     We need to use `httpx.AsyncClient` here, as `starlite.TestClient` creates its own event loop to
diff --git a/tests/integration/test_authors.py b/tests/integration/test_authors.py
index 8efd2f91..5cbe659d 100644
--- a/tests/integration/test_authors.py
+++ b/tests/integration/test_authors.py
@@ -3,13 +3,10 @@
 
 from typing import TYPE_CHECKING
 
-import pytest
-
 if TYPE_CHECKING:
     from httpx import AsyncClient
 
 
-@pytest.mark.xfail()
 async def test_update_author(client: AsyncClient) -> None:
     """Integration test for PUT route."""
     response = await client.put(
diff --git a/tests/integration/test_logging.py b/tests/integration/test_logging.py
index 7ec09c88..a3fe8678 100644
--- a/tests/integration/test_logging.py
+++ b/tests/integration/test_logging.py
@@ -48,7 +48,6 @@ async def test_logging(app: "Starlite", cap_logger: CapturingLogger) -> None:
                 "request": {
                     "path": "/authors/97108ac1-ffcb-411d-8b1e-d9183399f63b",
                     "method": "PUT",
-                    "content_type": ("application/json", {}),
                     "headers": {
                         "host": "testserver",
                         "accept": "*/*",
diff --git a/tests/pytest_plugin/__init__.py b/tests/pytest_plugin/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/pytest_plugin/conftest.py b/tests/pytest_plugin/conftest.py
new file mode 100644
index 00000000..0172cf77
--- /dev/null
+++ b/tests/pytest_plugin/conftest.py
@@ -0,0 +1,4 @@
+"""Enable the `pytester` fixture for the plugin tests."""
+from __future__ import annotations
+
+pytest_plugins = ["pytester"]
diff --git a/tests/pytest_plugin/test_plugin.py b/tests/pytest_plugin/test_plugin.py
new file mode 100644
index 00000000..75d00c93
--- /dev/null
+++ b/tests/pytest_plugin/test_plugin.py
@@ -0,0 +1,174 @@
+"""Test suite for pytest plugin."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from pytest import Pytester
+
+
+def test_pytest_addoption(pytester: Pytester) -> None:
+    """Test ini options added."""
+    pytester.makepyfile(
+        """
+        from pytest import Parser
+        from pytest_starlite_saqlalchemy import pytest_addoption
+
+        def test_pytest_addoption() -> None:
+            parser = Parser()
+            pytest_addoption(parser)
+            assert parser._ininames == ["test_app", "unit_test_pattern"]
+        """
+    )
+    result = pytester.runpytest()
+    result.assert_outcomes(passed=1)
+
+
+def test_is_unit_test_true(pytester: Pytester) -> None:
+    """Test is_unit_test fixture True conditions."""
+    pytester.makepyprojecttoml(
+        f"""
+        [tool.pytest.ini_options]
+        unit_test_pattern = "^{pytester.path}/test_is_unit_test_true.py$"
+        """
+    )
+    pytester.makepyfile(
+        """
+        from unittest.mock import MagicMock
+        from starlite_saqlalchemy.sqlalchemy_plugin import SQLAlchemyConfig
+        from starlite_saqlalchemy.worker import Worker
+
+        def test_is_unit_test_true(is_unit_test: bool) -> None:
+            assert is_unit_test is True
+
+        def test_patch_sqlalchemy_plugin() -> None:
+            assert isinstance(SQLAlchemyConfig.on_shutdown, MagicMock)
+
+        def test_patch_worker() -> None:
+            assert isinstance(Worker.on_app_startup, MagicMock)
+            assert isinstance(Worker.stop, MagicMock)
+        """
+    )
+    result = pytester.runpytest()
+    result.assert_outcomes(passed=3)
+
+
+def test_is_unit_test_false(pytester: Pytester) -> None:
+    """Unit is_unit_test fixture False conditions."""
+    pytester.makepyprojecttoml(
+        """
+        [tool.pytest.ini_options]
+        unit_test_pattern = "^definitely/not/the/path/to/test_is_unit_test_false.py$"
+        """
+    )
+    pytester.makepyfile(
+        """
+        from unittest.mock import MagicMock
+        from starlite_saqlalchemy.sqlalchemy_plugin import SQLAlchemyConfig
+        from starlite_saqlalchemy.worker import Worker
+
+        def test_is_unit_test_false(is_unit_test: bool) -> None:
+            assert is_unit_test is False
+
+        def test_patch_sqlalchemy_plugin() -> None:
+            assert not isinstance(SQLAlchemyConfig.on_shutdown, MagicMock)
+
+        def test_patch_worker() -> None:
+            assert not isinstance(Worker.on_app_startup, MagicMock)
+            assert not isinstance(Worker.stop, MagicMock)
+        """
+    )
+    result = pytester.runpytest()
+    result.assert_outcomes(passed=3)
+
+
+def test_patch_http_close(pytester: Pytester) -> None:
+    """Test that http clients won't be closed in-between tests."""
+    pytester.makepyfile(
+        """
+        import starlite_saqlalchemy
+
+        client = starlite_saqlalchemy.http.Client("https://somewhere.com")
+        assert starlite_saqlalchemy.http.clients
+
+        def test_patch_http_close(is_unit_test: bool) -> None:
+            assert not starlite_saqlalchemy.http.clients
+        """
+    )
+    result = pytester.runpytest()
+    result.assert_outcomes(passed=1)
+
+
+def test_app_fixture_if_app_factory(pytester: Pytester) -> None:
+    """Test that the app fixture returns an instance retrieved from a
+    factory."""
+    pytester.makepyprojecttoml(
+        """
+        [tool.pytest.ini_options]
+        test_app = "tests.utils.app:create_app"
+        """
+    )
+    pytester.makepyfile(
+        """
+        from starlite import Starlite
+
+        def test_app(app):
+            assert isinstance(app, Starlite)
+            assert "/authors" in app.route_handler_method_map
+        """
+    )
+    result = pytester.runpytest()
+    result.assert_outcomes(passed=1)
+
+
+def test_app_fixture_if_app_instance(pytester: Pytester) -> None:
+    """Test that the app fixture returns the an instance if the path points to
+    one."""
+    pytester.syspathinsert()
+    pytester.makepyfile(
+        test_app="""
+            from tests.utils.app import create_app
+
+            app = create_app()
+            """
+    )
+    pytester.makepyprojecttoml(
+        """
+        [tool.pytest.ini_options]
+        test_app = "test_app:app"
+        """
+    )
+    pytester.makepyfile(
+        """
+        from starlite import Starlite
+
+        def test_app(app):
+            assert isinstance(app, Starlite)
+            assert "/authors" in app.route_handler_method_map
+        """
+    )
+    result = pytester.runpytest()
+    result.assert_outcomes(passed=1)
+
+
+def test_app_fixture_if_test_app_path_does_not_exist(pytester: Pytester) -> None:
+    """Tests that the app fixture falls back to a new app instance if the
+    configured path is not found."""
+    pytester.makepyprojecttoml(
+        """
+        [tool.pytest.ini_options]
+        test_app = "definitely.not.the.path.to.app:app"
+        """
+    )
+    pytester.makepyfile(
+        """
+        from starlite import Starlite
+
+        def test_app(app):
+            assert isinstance(app, Starlite)
+            # the app that is created should not have any handlers attached.
+            assert app.route_handler_method_map.keys() == {"/health"}
+        """
+    )
+    result = pytester.runpytest()
+    result.assert_outcomes(passed=1)
diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py
index 386f6eab..16871add 100644
--- a/tests/unit/conftest.py
+++ b/tests/unit/conftest.py
@@ -2,15 +2,12 @@
 from __future__ import annotations
 
 from typing import TYPE_CHECKING
-from unittest.mock import MagicMock
 
 import pytest
 from saq.job import Job
 from starlite.datastructures import State
 from starlite.enums import ScopeType
-from starlite.testing import TestClient
 
-from starlite_saqlalchemy import sqlalchemy_plugin, worker
 from starlite_saqlalchemy.testing import GenericMockRepository
 from tests.utils.domain.authors import Author
 from tests.utils.domain.authors import Service as AuthorService
@@ -20,34 +17,11 @@
 from ..utils import controllers
 
 if TYPE_CHECKING:
-    from collections import abc
 
-    from pytest import MonkeyPatch
     from starlite import Starlite
     from starlite.types import HTTPResponseBodyEvent, HTTPResponseStartEvent, HTTPScope
 
 
-@pytest.fixture(scope="session", autouse=True)
-def _patch_sqlalchemy_plugin() -> abc.Iterator:
-    monkeypatch = pytest.MonkeyPatch()
-    monkeypatch.setattr(
-        sqlalchemy_plugin.SQLAlchemyConfig,  # type:ignore[attr-defined]
-        "on_shutdown",
-        MagicMock(),
-    )
-    yield
-    monkeypatch.undo()
-
-
-@pytest.fixture(scope="session", autouse=True)
-def _patch_worker() -> abc.Iterator:
-    monkeypatch = pytest.MonkeyPatch()
-    monkeypatch.setattr(worker.Worker, "on_app_startup", MagicMock())
-    monkeypatch.setattr(worker.Worker, "stop", MagicMock())
-    yield
-    monkeypatch.undo()
-
-
 @pytest.fixture(name="author_repository_type")
 def fx_author_repository_type(
     authors: list[Author], monkeypatch: pytest.MonkeyPatch
@@ -92,27 +66,6 @@ def fx_book_repository(
     return book_repository_type()
 
 
-@pytest.fixture(name="app")
-def fx_app(app: Starlite, monkeypatch: MonkeyPatch) -> Starlite:
-    """Remove service readiness checks for unit tests."""
-    monkeypatch.setattr(app, "before_startup", [])
-    return app
-
-
-@pytest.fixture(name="client")
-def fx_client(app: Starlite) -> abc.Iterator[TestClient]:
-    """Client instance attached to app.
-
-    Args:
-        app: The app for testing.
-
-    Returns:
-        Test client instance.
-    """
-    with TestClient(app=app) as client_:
-        yield client_
-
-
 @pytest.fixture()
 def http_response_start() -> HTTPResponseStartEvent:
     """ASGI message for start of response."""
diff --git a/tests/unit/test_init_plugin.py b/tests/unit/test_init_plugin.py
index a8a53bd2..d86ed868 100644
--- a/tests/unit/test_init_plugin.py
+++ b/tests/unit/test_init_plugin.py
@@ -8,7 +8,7 @@
 from starlite import Starlite
 from starlite.cache import SimpleCacheBackend
 
-from starlite_saqlalchemy import init_plugin
+from starlite_saqlalchemy import init_plugin, sentry
 
 if TYPE_CHECKING:
     from typing import Any
@@ -82,3 +82,15 @@ def test_ensure_list(in_: Any, out: Any) -> None:
     """Test _ensure_list() functionality."""
     # pylint: disable=protected-access
     assert init_plugin.ConfigureApp._ensure_list(in_) == out
+
+
+@pytest.mark.parametrize(
+    ("env", "exp"), [("dev", True), ("prod", True), ("local", False), ("test", False)]
+)
+def test_sentry_environment_gate(env: str, exp: bool, monkeypatch: MonkeyPatch) -> None:
+    """Test that the sentry integration is configured under different
+    environment names."""
+    monkeypatch.setattr(init_plugin, "IS_LOCAL_ENVIRONMENT", env == "local")
+    monkeypatch.setattr(init_plugin, "IS_TEST_ENVIRONMENT", env == "test")
+    app = Starlite(route_handlers=[], on_app_init=[init_plugin.ConfigureApp()])
+    assert bool(sentry.configure in app.on_startup) is exp  # noqa: SIM901
diff --git a/tests/unit/test_log.py b/tests/unit/test_log.py
index 0373b16a..f10ce12b 100644
--- a/tests/unit/test_log.py
+++ b/tests/unit/test_log.py
@@ -292,7 +292,6 @@ async def test_before_send_handler_extract_request_data(
     assert data == {
         "path": "/",
         "method": "POST",
-        "content_type": ("application/json", {}),
         "headers": {"content-length": "10", "content-type": "application/json"},
         "cookies": {},
         "query": b"",
@@ -436,7 +435,6 @@ def test_handler() -> str:
     assert "exception" not in call.kwargs
 
 
-@pytest.mark.xfail(reason="starlite is returning 500 for invalid payloads as of v1.48.1")
 async def test_log_request_with_invalid_json_payload(client: TestClient[Starlite]) -> None:
     """Test logs emitted with invalid client payload.
 
diff --git a/tests/unit/test_scripts.py b/tests/unit/test_scripts.py
index 452e2911..bb950e57 100644
--- a/tests/unit/test_scripts.py
+++ b/tests/unit/test_scripts.py
@@ -1,27 +1,33 @@
 """Tests for scripts.py."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
 
 import pytest
 
-from starlite_saqlalchemy import settings
-from starlite_saqlalchemy.scripts import determine_reload_dirs, determine_should_reload
+from starlite_saqlalchemy import scripts, settings
 from starlite_saqlalchemy.testing import modify_settings
 
+if TYPE_CHECKING:
+    from pytest import MonkeyPatch
+
 
 @pytest.mark.parametrize(("reload", "expected"), [(None, True), (True, True), (False, False)])
-def test_uvicorn_config_auto_reload_local(reload: bool | None, expected: bool) -> None:
+def test_uvicorn_config_auto_reload_local(
+    reload: bool | None, expected: bool, monkeypatch: MonkeyPatch
+) -> None:
     """Test that setting ENVIRONMENT to 'local' triggers auto reload."""
-    with modify_settings(
-        (settings.app, {"ENVIRONMENT": "local"}), (settings.server, {"RELOAD": reload})
-    ):
-        assert determine_should_reload() is expected
+    monkeypatch.setattr(scripts, "IS_LOCAL_ENVIRONMENT", True)
+    with modify_settings((settings.server, {"RELOAD": reload})):
+        assert scripts.determine_should_reload() is expected
 
 
 @pytest.mark.parametrize("reload", [True, False])
 def test_uvicorn_config_reload_dirs(reload: bool) -> None:
     """Test that RELOAD_DIRS is only used when RELOAD is enabled."""
     if not reload:
-        assert determine_reload_dirs(reload) is None
+        assert scripts.determine_reload_dirs(reload) is None
     else:
-        reload_dirs = determine_reload_dirs(reload)
+        reload_dirs = scripts.determine_reload_dirs(reload)
         assert reload_dirs is not None
         assert reload_dirs == settings.server.RELOAD_DIRS
diff --git a/tests/unit/test_sentry.py b/tests/unit/test_sentry.py
new file mode 100644
index 00000000..398c09e9
--- /dev/null
+++ b/tests/unit/test_sentry.py
@@ -0,0 +1,24 @@
+"""Tests for sentry integration."""
+
+from typing import TYPE_CHECKING
+
+import pytest
+
+from starlite_saqlalchemy import settings
+from starlite_saqlalchemy.sentry import SamplingContext, sentry_traces_sampler
+
+if TYPE_CHECKING:
+    from starlite.types.asgi_types import HTTPScope
+
+
+@pytest.mark.parametrize(
+    ("path", "sample_rate"),
+    [("/watever", settings.sentry.TRACES_SAMPLE_RATE), (settings.api.HEALTH_PATH, 0.0)],
+)
+def test_sentry_traces_sampler(http_scope: "HTTPScope", path: str, sample_rate: float) -> None:
+    """Test that traces sampler correctly ignore health requests."""
+    http_scope["path"] = path
+    sentry_context = SamplingContext(
+        asgi_scope=http_scope, parent_sampled=None, transaction_context={}
+    )
+    assert sentry_traces_sampler(sentry_context) == sample_rate
diff --git a/tests/unit/test_testing.py b/tests/unit/test_testing.py
index 519f5d5f..2474f3ba 100644
--- a/tests/unit/test_testing.py
+++ b/tests/unit/test_testing.py
@@ -6,7 +6,11 @@
 
 import httpx
 import pytest
-from starlite.status_codes import HTTP_200_OK, HTTP_404_NOT_FOUND
+from starlite.status_codes import (
+    HTTP_200_OK,
+    HTTP_404_NOT_FOUND,
+    HTTP_405_METHOD_NOT_ALLOWED,
+)
 
 from starlite_saqlalchemy import testing
 from starlite_saqlalchemy.exceptions import ConflictError, StarliteSaqlalchemyError
@@ -210,3 +214,12 @@ def test_tester_run_method(params: dict[str, Any] | None) -> None:
             ("test_member_request", ("POST", "create", 201), {}),
             ("test_member_request", ("DELETE", "delete", 200), {}),
         ]
+
+
+def test_tester_ignores_405_response(
+    tester: testing.ControllerTest, mock_response: MagicMock
+) -> None:
+    """Test that 405 responses don't raise from asserts."""
+    mock_response.status_code = HTTP_405_METHOD_NOT_ALLOWED
+    tester.test_get_collection()
+    tester.test_member_request("GET", "get", HTTP_200_OK)
diff --git a/tests/utils/app.py b/tests/utils/app.py
new file mode 100644
index 00000000..f821df42
--- /dev/null
+++ b/tests/utils/app.py
@@ -0,0 +1,16 @@
+"""Test application."""
+from __future__ import annotations
+
+from starlite import Starlite
+
+from starlite_saqlalchemy import ConfigureApp
+
+from . import controllers
+
+
+def create_app() -> Starlite:
+    """App for our test domain."""
+    return Starlite(
+        route_handlers=[controllers.create_router()],
+        on_app_init=[ConfigureApp()],
+    )
diff --git a/tests/utils/controllers.py b/tests/utils/controllers.py
index d2898450..7f456178 100644
--- a/tests/utils/controllers.py
+++ b/tests/utils/controllers.py
@@ -4,7 +4,7 @@
 from uuid import UUID
 
 from sqlalchemy.ext.asyncio import AsyncSession
-from starlite import Dependency, delete, get, post, put
+from starlite import Dependency, Provide, Router, delete, get, post, put
 from starlite.status_codes import HTTP_200_OK
 
 from starlite_saqlalchemy.repository.types import FilterTypes
@@ -49,3 +49,18 @@ async def update_author(data: WriteDTO, service: Service, author_id: UUID) -> Re
 async def delete_author(service: Service, author_id: UUID) -> ReadDTO:
     """Delete Author by ID."""
     return ReadDTO.from_orm(await service.delete(author_id))
+
+
+def create_router() -> Router:
+    """Create a router for our test domain controllers."""
+    return Router(
+        path="/authors",
+        route_handlers=[
+            create_author,
+            delete_author,
+            get_author,
+            get_authors,
+            update_author,
+        ],
+        dependencies={"service": Provide(provides_service)},
+    )