Repository: lwinterface/panini (language: python)

Issue:
Remove sync from panini TestClient
Not more needed
Naming issue in check_logger - use check_logger_connected instead
We should always use transparent & meaningful names
https://github.com/lwinterface/panini/blob/a098cdf07c07dff3d1984555dbec798b8e61ef96/panini/utils/logger.py#L15
Minor issue - remove panini app's arguments 'app_strategy', 'num_of_queues', 'publish_subjects'
because not relevant anymore
Fix names for subscribe_new_subject (remove old `aio` interface)

Add test for long requests (to check, if new TestClient is working properly with not immediate requests)

No hinting in publish & request funcitons
Due to middlewares - there is no hinting for `app.publish` & `app.request` funcitons
Non-blocking request support
Add possibility to send non-blocking request (if callback provided inside the request)
Panini changelog
It would be great to provide changelog to know, which changes in which versions were done
Added listen_subject_only_if_exclude parameter for excluding unnecess…
…ary subjects


Discussion / hints:

Fix patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index ea4a744..751ecd4 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,31 +1,69 @@
 version: 2.1
-jobs:
-  test:
-    machine:
-      image: ubuntu-2004:202010-01
-      docker_layer_caching: true
-    working_directory: ~/panini/
+parameters:
+  repo:
+    type: string
+    default: "panini"
+
+executors:
+  executor-for-panini:
+    parameters:
+      version:
+        type: string
+        default: "3.8"
+    docker:
+      - image: python:<< parameters.version >>-slim-buster
+      - image: nats
+
+commands:
+  install-requirements:
     steps:
-      - checkout
       - run:
-          name: Installing libraries
-          command: |
-            pip3 install pytest
-            pip3 install -r requirements/defaults.txt
-      - run:
-          name: Start containers nats
-          command: |
-            docker run -d -p 4222:4222 -p 6222:6222 -p 8222:8222 nats
+          name: Install requirements
+          command: pip3 install pytest && pip3 install -r requirements/defaults.txt
+  run-tests:
+    steps:
       - run:
-          name: Run Tests
+          name: Run tests
           command: |
-            cd ~/panini
             mkdir results
             timeout 300 python3 -m pytest --junitxml=results/out.xml || (echo "Doesn't finished succefully" && exit 1)
+  store-results:
+    steps:
       - store_test_results:
           path: results
-    path:
+
+working_directory: ~/<< pipeline.parameters.repo >>
+
+jobs:
+  test:
+    parameters:
+      version:
+        description: "python version tag"
+        default: "3.8"
+        type: string
+    executor:
+      name: executor-for-panini
+      version: <<parameters.version>>
+    steps:
+      - checkout
+      - when:
+          condition: 
+            matches: { pattern: "3.10.*", value: <<parameters.version>> }
+          steps:
+            - run: 
+                command: |
+                  apt update && apt install -y libc6-dev gcc build-essential
+                  sed -i 's/1.6.1/1.6.3/' requirements/defaults.txt
+      - install-requirements
+      - run-tests
+      - store-results
 workflows:
   main:
     jobs:
-      - test
+      - test:
+          matrix:
+            parameters:
+              version:
+                - "3.8"
+                - "3.9"
+#                - "3.10.0b1"
diff --git a/.gitignore b/.gitignore
index 0081739..26d2846 100644
--- a/.gitignore
+++ b/.gitignore
@@ -134,4 +134,5 @@ dmypy.json
 **/logs
 **/log_config.json
 **/test_logs
-**/test_logger_logs
\ No newline at end of file
+**/test_logger_logs
+**/resources/
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..c6e210d
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,236 @@
+# Changelog
+
+## [Unreleased](https://github.com/lwinterface/panini/tree/HEAD)
+
+[Full Changelog](https://github.com/lwinterface/panini/compare/v0.3.0...HEAD)
+
+**Implemented enhancements:**
+
+- Naming issue in check\_logger - use check\_logger\_connected instead [\#148](https://github.com/lwinterface/panini/issues/148)
+- Cut sync strategy [\#137](https://github.com/lwinterface/panini/issues/137)
+
+**Fixed bugs:**
+
+- TestClient problem on @client.listen - do not wait, while publishing response back [\#153](https://github.com/lwinterface/panini/issues/153)
+
+**Closed issues:**
+
+- Re-add test for allocation queue group [\#157](https://github.com/lwinterface/panini/issues/157)
+- Fix names for subscribe\_new\_subject \(remove old `aio` interface\) [\#151](https://github.com/lwinterface/panini/issues/151)
+- Remove sync from panini TestClient [\#150](https://github.com/lwinterface/panini/issues/150)
+- Minor issue - remove panini app's arguments 'app\_strategy', 'num\_of\_queues', 'publish\_subjects' [\#149](https://github.com/lwinterface/panini/issues/149)
+
+## [v0.3.0](https://github.com/lwinterface/panini/tree/v0.3.0) (2021-04-27)
+
+[Full Changelog](https://github.com/lwinterface/panini/compare/v0.2.3...v0.3.0)
+
+**Implemented enhancements:**
+
+- release 0.2.3 [\#139](https://github.com/lwinterface/panini/pull/139) ([artas728](https://github.com/artas728))
+- From json to ujson [\#117](https://github.com/lwinterface/panini/pull/117) ([artas728](https://github.com/artas728))
+
+**Fixed bugs:**
+
+- Emulator examples does not work: ModuleNotFoundError: No module named 'panini.emulator' [\#145](https://github.com/lwinterface/panini/issues/145)
+- TestClient timed out socket bug [\#142](https://github.com/lwinterface/panini/issues/142)
+- bool fields fix [\#135](https://github.com/lwinterface/panini/pull/135) ([artas728](https://github.com/artas728))
+- Ujson in requirements [\#122](https://github.com/lwinterface/panini/pull/122) ([artas728](https://github.com/artas728))
+- from warn logger to print for experimental warning in emulator [\#116](https://github.com/lwinterface/panini/pull/116) ([artas728](https://github.com/artas728))
+
+**Merged pull requests:**
+
+- Removed sync app strategy [\#147](https://github.com/lwinterface/panini/pull/147) ([artas728](https://github.com/artas728))
+- Cut sync strategy [\#146](https://github.com/lwinterface/panini/pull/146) ([danylott](https://github.com/danylott))
+- \#142 - added auto-reconnect parameter to TestClient, that will reconn… [\#143](https://github.com/lwinterface/panini/pull/143) ([danylott](https://github.com/danylott))
+- Bug/logger not working until app start [\#136](https://github.com/lwinterface/panini/pull/136) ([danylott](https://github.com/danylott))
+- hotfix for test\_wss - added sleep waiting for websockets [\#132](https://github.com/lwinterface/panini/pull/132) ([danylott](https://github.com/danylott))
+- Circleci project setup [\#129](https://github.com/lwinterface/panini/pull/129) ([bugimprover](https://github.com/bugimprover))
+- \#125 - fixed test\_middleware\_manager - make independent testing middl… [\#128](https://github.com/lwinterface/panini/pull/128) ([danylott](https://github.com/danylott))
+- \#118 - renamed: app.\_client\_id -\> app.client\_id in PrometheusMonitori… [\#121](https://github.com/lwinterface/panini/pull/121) ([danylott](https://github.com/danylott))
+- \#101 - added posibility to pass args and kwargs to TestClient run\_panini [\#112](https://github.com/lwinterface/panini/pull/112) ([danylott](https://github.com/danylott))
+- \#109 - fix requirements prometheus client on develop [\#110](https://github.com/lwinterface/panini/pull/110) ([danylott](https://github.com/danylott))
+- changed ":" to "-" for emulator files [\#108](https://github.com/lwinterface/panini/pull/108) ([oleksii-v](https://github.com/oleksii-v))
+
+## [v0.2.3](https://github.com/lwinterface/panini/tree/v0.2.3) (2021-04-06)
+
+[Full Changelog](https://github.com/lwinterface/panini/compare/v0.2.2...v0.2.3)
+
+**Implemented enhancements:**
+
+- Use ujson instead of json to increase performance [\#114](https://github.com/lwinterface/panini/issues/114)
+- app.logger and get\_logger\(\) is not working untill app.start\(\) [\#113](https://github.com/lwinterface/panini/issues/113)
+- add posibility to pass args and kwargs to TestClient run\_panini [\#101](https://github.com/lwinterface/panini/issues/101)
+- Middleware that count amount of request by status [\#82](https://github.com/lwinterface/panini/issues/82)
+- Use labels in ListenPerformanceMonitoring middleware [\#81](https://github.com/lwinterface/panini/issues/81)
+
+**Fixed bugs:**
+
+- Bool field in validator doesn't work [\#134](https://github.com/lwinterface/panini/issues/134)
+- Panini tests do not pass on Ubuntu with `pytest` command [\#125](https://github.com/lwinterface/panini/issues/125)
+- ModuleNotFoundError: No module named 'ujson' [\#120](https://github.com/lwinterface/panini/issues/120)
+- Error in PrometheusMonitoringMiddleware: AttributeError: 'App' object has no attribute '\_client\_id' [\#118](https://github.com/lwinterface/panini/issues/118)
+- fix requirements prometheus client on develop [\#109](https://github.com/lwinterface/panini/issues/109)
+- Error while cloning panini: error: unable to create file examples/emulator/resources/events.listener.2021-03-19-16:21:13.jsonl: Invalid argument [\#107](https://github.com/lwinterface/panini/issues/107)
+- RuntimeWarning in time sending panini\_event.\*.\*.started with NATSTimeoutMiddleware [\#98](https://github.com/lwinterface/panini/issues/98)
+- Fix app.\_start\_tasks [\#88](https://github.com/lwinterface/panini/issues/88)
+- test\_client sleep\_time - depends on computer power [\#79](https://github.com/lwinterface/panini/issues/79)
+- None does not have SIGKILL [\#28](https://github.com/lwinterface/panini/issues/28)
+
+**Closed issues:**
+
+- Integrate emulation system [\#49](https://github.com/lwinterface/panini/issues/49)
+- Additional\(optional\) datatypes for messages: str, bytes [\#35](https://github.com/lwinterface/panini/issues/35)
+- ./run\_test.sh - modify tests to run with simple pytest command, instead of shell script [\#27](https://github.com/lwinterface/panini/issues/27)
+-  Change arguments format in endpoints [\#25](https://github.com/lwinterface/panini/issues/25)
+- Not logic sub title [\#13](https://github.com/lwinterface/panini/issues/13)
+
+**Merged pull requests:**
+
+- Release v0.2.3 [\#140](https://github.com/lwinterface/panini/pull/140) ([artas728](https://github.com/artas728))
+
+## [v0.2.2](https://github.com/lwinterface/panini/tree/v0.2.2) (2021-03-24)
+
+[Full Changelog](https://github.com/lwinterface/panini/compare/v0.2.1...v0.2.2)
+
+**Implemented enhancements:**
+
+- Minor fix: change grafana dashboard default rate size from 1m to 15m [\#102](https://github.com/lwinterface/panini/issues/102)
+- Panini dashboard [\#83](https://github.com/lwinterface/panini/issues/83)
+- Release v0.2.2 [\#105](https://github.com/lwinterface/panini/pull/105) ([artas728](https://github.com/artas728))
+- Release v0.2.2 [\#104](https://github.com/lwinterface/panini/pull/104) ([artas728](https://github.com/artas728))
+- 1m default rate size changed to 15m [\#103](https://github.com/lwinterface/panini/pull/103) ([artas728](https://github.com/artas728))
+
+**Fixed bugs:**
+
+- :wrench: app selection fix \#16 [\#96](https://github.com/lwinterface/panini/pull/96) ([artas728](https://github.com/artas728))
+
+**Merged pull requests:**
+
+- Emulator [\#99](https://github.com/lwinterface/panini/pull/99) ([oleksii-v](https://github.com/oleksii-v))
+- Fix test client sleep time [\#97](https://github.com/lwinterface/panini/pull/97) ([danylott](https://github.com/danylott))
+- Panini dashboard [\#95](https://github.com/lwinterface/panini/pull/95) ([artas728](https://github.com/artas728))
+- :chart\_with\_upwards\_trend: added grafana dashboard [\#94](https://github.com/lwinterface/panini/pull/94) ([artas728](https://github.com/artas728))
+- version updated to 0.2.1 [\#93](https://github.com/lwinterface/panini/pull/93) ([artas728](https://github.com/artas728))
+
+## [v0.2.1](https://github.com/lwinterface/panini/tree/v0.2.1) (2021-03-17)
+
+[Full Changelog](https://github.com/lwinterface/panini/compare/v0.2.0...v0.2.1)
+
+**Implemented enhancements:**
+
+- Initialize logger only at \_start\(\) [\#67](https://github.com/lwinterface/panini/issues/67)
+
+**Fixed bugs:**
+
+- How to request default logger if there is no access too "app" object? [\#51](https://github.com/lwinterface/panini/issues/51)
+
+**Merged pull requests:**
+
+- Monitoring middleware [\#92](https://github.com/lwinterface/panini/pull/92) ([artas728](https://github.com/artas728))
+- hotfix: send Panini event "started" only for asyncio app\_strategy [\#91](https://github.com/lwinterface/panini/pull/91) ([artas728](https://github.com/artas728))
+- Send message when NATSClient started [\#90](https://github.com/lwinterface/panini/pull/90) ([artas728](https://github.com/artas728))
+- Prometheus monitoring middleware [\#86](https://github.com/lwinterface/panini/pull/86) ([danylott](https://github.com/danylott))
+- \#67 - run logger on app.\_start\(\), added test\_client.stop\(\) method, im… [\#80](https://github.com/lwinterface/panini/pull/80) ([danylott](https://github.com/danylott))
+- added labels to ListenerMonitoring [\#75](https://github.com/lwinterface/panini/pull/75) ([artas728](https://github.com/artas728))
+- For v0.2.0 [\#70](https://github.com/lwinterface/panini/pull/70) ([artas728](https://github.com/artas728))
+
+## [v0.2.0](https://github.com/lwinterface/panini/tree/v0.2.0) (2021-03-12)
+
+[Full Changelog](https://github.com/lwinterface/panini/compare/0.1.2...v0.2.0)
+
+**Fixed bugs:**
+
+- ListenPerformancePrometheusTracerMiddleware doesn't work [\#59](https://github.com/lwinterface/panini/issues/59)
+- Middlewares don't work for dynamic listeners [\#58](https://github.com/lwinterface/panini/issues/58)
+- send\_any, send\_publish, send\_request in one middleware [\#41](https://github.com/lwinterface/panini/issues/41)
+- Remove annotations on message for publish and request - because for now we use more than just dict [\#38](https://github.com/lwinterface/panini/issues/38)
+- Middlewares for dynamic listeners and ListenPerformancePrometheusTracerMiddleware minor fix  [\#62](https://github.com/lwinterface/panini/pull/62) ([artas728](https://github.com/artas728))
+
+**Closed issues:**
+
+- Rename ListenPerformancePrometheusTracerMiddleware to something shorter [\#57](https://github.com/lwinterface/panini/issues/57)
+- Change log.warning -\> log.info in simple\_examples, because we use for now log.info as default console format [\#52](https://github.com/lwinterface/panini/issues/52)
+- How to deliver build-in middlewares? [\#50](https://github.com/lwinterface/panini/issues/50)
+- Make TestClient listen interface the same, as in app.listen\(\) [\#30](https://github.com/lwinterface/panini/issues/30)
+
+**Merged pull requests:**
+
+- Added new packages and dependencies [\#73](https://github.com/lwinterface/panini/pull/73) ([artas728](https://github.com/artas728))
+- added new packages and dependencies [\#72](https://github.com/lwinterface/panini/pull/72) ([artas728](https://github.com/artas728))
+- Listen monitoring fix [\#71](https://github.com/lwinterface/panini/pull/71) ([artas728](https://github.com/artas728))
+- Listen monitoring percentile [\#69](https://github.com/lwinterface/panini/pull/69) ([danylott](https://github.com/danylott))
+- quickfix - use Msg in test\_client [\#68](https://github.com/lwinterface/panini/pull/68) ([danylott](https://github.com/danylott))
+-  \#30 - client.listen interface now is the same as in nats\_client \(usi… [\#66](https://github.com/lwinterface/panini/pull/66) ([danylott](https://github.com/danylott))
+- \#38 - improved nats\_client interface, removed annotations from message [\#65](https://github.com/lwinterface/panini/pull/65) ([danylott](https://github.com/danylott))
+- get\_logger default parameter and log.warning -\> log.info in examples  [\#64](https://github.com/lwinterface/panini/pull/64) ([danylott](https://github.com/danylott))
+- Built in middlewares NATSTimeout & ListenPerfomanceMonitoring [\#55](https://github.com/lwinterface/panini/pull/55) ([artas728](https://github.com/artas728))
+- Built in middlewares [\#54](https://github.com/lwinterface/panini/pull/54) ([danylott](https://github.com/danylott))
+
+## [0.1.2](https://github.com/lwinterface/panini/tree/0.1.2) (2021-03-10)
+
+[Full Changelog](https://github.com/lwinterface/panini/compare/v0.1.3...0.1.2)
+
+## [v0.1.3](https://github.com/lwinterface/panini/tree/v0.1.3) (2021-03-10)
+
+[Full Changelog](https://github.com/lwinterface/panini/compare/0.1.1...v0.1.3)
+
+**Implemented enhancements:**
+
+- Move validators to pydantic [\#18](https://github.com/lwinterface/panini/issues/18)
+- Test validators [\#11](https://github.com/lwinterface/panini/issues/11)
+
+**Fixed bugs:**
+
+- Test client does not show errors from panini side - only errors, that returns back or logged [\#46](https://github.com/lwinterface/panini/issues/46)
+
+**Closed issues:**
+
+- Test different datatypes functionality [\#48](https://github.com/lwinterface/panini/issues/48)
+- Update in README for middlewares [\#42](https://github.com/lwinterface/panini/issues/42)
+- test wss example [\#23](https://github.com/lwinterface/panini/issues/23)
+- Use Black8 as default project formatter [\#21](https://github.com/lwinterface/panini/issues/21)
+- Default logger level  [\#16](https://github.com/lwinterface/panini/issues/16)
+- Used Python Libraries [\#14](https://github.com/lwinterface/panini/issues/14)
+- Change word "topic" to "subject" [\#12](https://github.com/lwinterface/panini/issues/12)
+- slow customers  [\#4](https://github.com/lwinterface/panini/issues/4)
+- Middleware [\#3](https://github.com/lwinterface/panini/issues/3)
+
+**Merged pull requests:**
+
+- \#46 - added error logging for panini testclient [\#47](https://github.com/lwinterface/panini/pull/47) ([danylott](https://github.com/danylott))
+- :coconut: few words about middlewares [\#45](https://github.com/lwinterface/panini/pull/45) ([artas728](https://github.com/artas728))
+- Middleware integration [\#40](https://github.com/lwinterface/panini/pull/40) ([artas728](https://github.com/artas728))
+- make tests independent - to run it simply using pytest command [\#36](https://github.com/lwinterface/panini/pull/36) ([danylott](https://github.com/danylott))
+- Bump aiohttp from 3.6.3 to 3.7.4 in /examples/dockercompose\_project/microservice1 [\#33](https://github.com/lwinterface/panini/pull/33) ([dependabot[bot]](https://github.com/apps/dependabot))
+- \#21 - applied black formatter, flake8 python PEP8 principles and  [\#32](https://github.com/lwinterface/panini/pull/32) ([danylott](https://github.com/danylott))
+- Test validators wss \#11, \#23 [\#31](https://github.com/lwinterface/panini/pull/31) ([danylott](https://github.com/danylott))
+- middleware\_dict using msg object functionality [\#29](https://github.com/lwinterface/panini/pull/29) ([danylott](https://github.com/danylott))
+- :wrench: readme minor fix [\#24](https://github.com/lwinterface/panini/pull/24) ([artas728](https://github.com/artas728))
+- Added middleware dict feature -  [\#22](https://github.com/lwinterface/panini/pull/22) ([danylott](https://github.com/danylott))
+- Added dynamic subscribe/unsubscribe [\#20](https://github.com/lwinterface/panini/pull/20) ([artas728](https://github.com/artas728))
+- Bump aiohttp from 3.6.3 to 3.7.4 in /requirements [\#15](https://github.com/lwinterface/panini/pull/15) ([dependabot[bot]](https://github.com/apps/dependabot))
+
+## [0.1.1](https://github.com/lwinterface/panini/tree/0.1.1) (2021-02-26)
+
+[Full Changelog](https://github.com/lwinterface/panini/compare/7751489a13af06fba290a64dec0d224e8bc171ab...0.1.1)
+
+**Fixed bugs:**
+
+- Tests on Mac & Windows platform [\#8](https://github.com/lwinterface/panini/issues/8)
+- Logger in separate process error on Mac & Windows [\#6](https://github.com/lwinterface/panini/issues/6)
+- Windows support [\#5](https://github.com/lwinterface/panini/issues/5)
+- having a root logging [\#1](https://github.com/lwinterface/panini/issues/1)
+
+**Closed issues:**
+
+- Default logging in the main process [\#10](https://github.com/lwinterface/panini/issues/10)
+
+**Merged pull requests:**
+
+- \#8 fix tests for support Mac & Windows - fixed using pytest.fixture\(s… [\#9](https://github.com/lwinterface/panini/pull/9) ([danylott](https://github.com/danylott))
+- \#5 \#6 issues handled [\#7](https://github.com/lwinterface/panini/pull/7) ([danylott](https://github.com/danylott))
+- Testclient [\#2](https://github.com/lwinterface/panini/pull/2) ([danylott](https://github.com/danylott))
+
+
+
+\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
diff --git a/examples/simple_examples/_wss_manager.py b/examples/simple_examples/_wss_manager.py
index 90059c3..d5a5477 100644
--- a/examples/simple_examples/_wss_manager.py
+++ b/examples/simple_examples/_wss_manager.py
@@ -65,7 +65,7 @@ async def client_listener(self, client_ws_connection, connection_id):
                 if action == "subscribe":
                     for subject in subjects:
                         cb = await self._get_callback(client_ws_connection)
-                        ssid = await self.app.aio_subscribe_new_subject(subject, cb)
+                        ssid = await self.app.subscribe_new_subject(subject, cb)
                         if subject not in self.ssid_map:
                             self.ssid_map[subject] = []
                         self.ssid_map[subject].append(ssid)
@@ -81,7 +81,7 @@ async def client_listener(self, client_ws_connection, connection_id):
                     for subject in subjects:
                         if subject in self.ssid_map:
                             for ssid in self.ssid_map[subject]:
-                                await self.app.aio_unsubscribe_ssid(ssid)
+                                await self.app.unsubscribe_ssid(ssid)
                             await client_ws_connection.send_str(
                                 json.dumps(
                                     {
diff --git a/examples/simple_examples/async_non_blocking_request.py b/examples/simple_examples/async_non_blocking_request.py
new file mode 100644
index 0000000..d774deb
--- /dev/null
+++ b/examples/simple_examples/async_non_blocking_request.py
@@ -0,0 +1,63 @@
+import asyncio
+import time
+from panini import app as panini_app
+
+app = panini_app.App(
+    service_name="async_sender",
+    host="127.0.0.1",
+    port=4222,
+)
+log = app.logger
+
+started_time = None
+msg_num = 10
+current_msg_num = 0
+
+msg = {
+    "key1": "value1",
+    "key2": 2,
+    "key3": 3.0,
+    "key4": [1, 2, 3, 4],
+    "key5": {"1": 1, "2": 2, "3": 3, "4": 4, "5": 5},
+    "key6": {"subkey1": "1", "subkey2": 2, "3": 3, "4": 4, "5": 5},
+    "key7": None,
+}
+
+
+@app.task()
+async def request_to_another_topic():
+    global started_time
+    started_time = time.monotonic()
+
+    def handle_response(msg):
+        """Note, that here msg.data will be in bytes"""
+        global current_msg_num
+        current_msg_num += 1
+        log.info(f"MSG: data: {msg.data}, data type: {type(msg.data)}")
+        log.info(f"<<<= response num {current_msg_num}")
+
+    for _ in range(msg_num):
+        await app.request(
+            message=msg, subject="some.topic.to.request", callback=handle_response
+        )
+        log.info(f"=>>> request num {_}")
+    print(f"Sent requests duration = {time.monotonic() - started_time}")
+    await app.publish(message={}, subject="finalize.it")
+
+
+@app.listen("some.topic.to.request")
+async def subject_for_requests_listener(msg):
+    await asyncio.sleep(1)
+    return {"success": True, "data": "request has been processed"}
+
+
+@app.listen("finalize.it")
+async def finalizer(msg):
+    while int(current_msg_num) < int(msg_num):
+        await asyncio.sleep(0.005)
+    duration = time.monotonic() - started_time
+    print(f"Handling {msg_num} requests  took: {duration} sec")
+
+
+if __name__ == "__main__":
+    app.start()
diff --git a/examples/simple_examples/debug_middleware_example.py b/examples/simple_examples/debug_middleware_example.py
new file mode 100644
index 0000000..5501139
--- /dev/null
+++ b/examples/simple_examples/debug_middleware_example.py
@@ -0,0 +1,35 @@
+from panini import app as panini_app
+from panini.middleware.debug_middleware import DebugMiddleware
+
+app = panini_app.App(
+    service_name="debug_middleware_example",
+    host="127.0.0.1",
+    port=4222,
+)
+
+
+message = {
+    "key1": "value1",
+    "key2": 2,
+    "key3": 3.0,
+    "key4": [1, 2, 3, 4],
+    "key5": {"1": 1, "2": 2, "3": 3, "4": 4, "5": 5},
+    "key6": {"subkey1": "1", "subkey2": 2, "3": 3, "4": 4, "5": 5},
+    "key7": None,
+}
+
+
+@app.task()
+async def publish():
+    for _ in range(10):
+        await app.request(subject="some.publish.subject", message=message)
+
+
+@app.listen("some.publish.subject")
+async def receive_messages(msg):
+    return {"success": True}
+
+
+if __name__ == "__main__":
+    app.add_middleware(DebugMiddleware, log_level="info")
+    app.start()
diff --git a/panini/app.py b/panini/app.py
index d3ca4d3..b7999bf 100644
--- a/panini/app.py
+++ b/panini/app.py
@@ -19,6 +19,7 @@
     InitializingTaskError,
     InitializingIntervalTaskError,
 )
+from .middleware.error import ErrorMiddleware
 from .utils.helper import (
     start_thread,
     get_app_root_path,
@@ -42,10 +43,10 @@ def __init__(
         reconnect: bool = False,
         max_reconnect_attempts: int = 60,
         reconnecting_time_sleep: int = 2,
-        app_strategy: str = "asyncio",
         subscribe_subjects_and_callbacks: dict = None,
         allocation_queue_group: str = "",
         listen_subject_only_if_include: list = None,
+        listen_subject_only_if_exclude: list = None,
         web_server: bool = False,
         web_app: web.Application = None,
         web_host: str = None,
@@ -53,6 +54,7 @@ def __init__(
         logger_required: bool = True,
         logger_files_path: str = None,
         logger_in_separate_process: bool = False,
+        pending_bytes_limit=65536 * 1024 * 10,
     ):
         """
         :param host: NATS broker host
@@ -63,7 +65,6 @@ def __init__(
         :param reconnect: allows reconnect if connection to NATS has been lost
         :param max_reconnect_attempts: number of reconnect attempts
         :param reconnecting_time_sleep: pause between reconnection
-        :param app_strategy:  'asyncio' only - but we let it with warning for backward parameter support
         :param subscribe_subjects_and_callbacks: if you need to subscribe additional
                                         subjects(except subjects from event.py).
                                         This way doesn't support validators
@@ -71,6 +72,8 @@ def __init__(
                                     more detailed here: https://docs.nats.io/nats-concepts/queue
         :param listen_subject_only_if_include:   if not None, client will subscribe
                                                 only to subjects that include these key words
+        :param listen_subject_only_if_exclude:   if not None, client will not subscribe
+                                                 to subjects that include these key words
         :param web_app: web.Application:       custom aiohttp app that you can create separately from panini.
                             if you set this argument client will only run this aiohttp app without handling
         :param web_host: Web application host
@@ -91,7 +94,6 @@ def __init__(
             os.environ["CLIENT_ID"] = client_id
             self.client_id = client_id
             self.service_name = service_name
-            self.app_strategy = app_strategy
             self.nats_config = {
                 "host": host,
                 "port": port,
@@ -101,9 +103,15 @@ def __init__(
                 "queue": allocation_queue_group,
                 "max_reconnect_attempts": max_reconnect_attempts,
                 "reconnecting_time_wait": reconnecting_time_sleep,
+                "pending_bytes_limit": pending_bytes_limit,
             }
             self.tasks = tasks
             self.listen_subject_only_if_include = listen_subject_only_if_include
+            self.listen_subject_only_if_exclude = listen_subject_only_if_exclude
+            assert (
+                self.listen_subject_only_if_include is None
+                or self.listen_subject_only_if_exclude is None
+            ), "You can use only 1 of listen_subject_only_if_include/exclude at the same moment!"
             self.subscribe_subjects_and_callbacks = subscribe_subjects_and_callbacks
 
             self.app_root_path = get_app_root_path()
@@ -131,10 +139,6 @@ def __init__(
                     self.client_id,
                 )
 
-            if app_strategy != "asyncio":
-                self.logger.warning(
-                    "Only 'asyncio' strategy is now supported. The app will run in asyncio mode!"
-                )
             self.logger_process = None
             self.log_stop_event = None
             self.log_listener_queue = None
@@ -195,6 +199,19 @@ def set_logger(
         self.logger.logger = logging.getLogger(service_name)
 
     def start(self):
+        if (
+            os.environ.get("PANINI_TEST_MODE")
+            and os.environ.get("PANINI_TEST_MODE_USE_ERROR_MIDDLEWARE", "false")
+            == "true"
+        ):
+
+            def exception_handler(e, **kwargs):
+                self.logger.exception(f"Error: {e}, for kwargs: {kwargs}")
+                raise
+
+            self.add_middleware(
+                ErrorMiddleware, error=Exception, callback=exception_handler
+            )
         if self.logger_required and self.logger_in_separate_process:
             self.set_logger(
                 self.service_name,
@@ -216,6 +233,15 @@ def start(self):
                             break
                     if success is False:
                         del subjects_and_callbacks[subject]
+            if self.listen_subject_only_if_exclude is not None:
+                for subject in subjects_and_callbacks.copy():
+                    success = True
+                    for subject_exclude in self.listen_subject_only_if_exclude:
+                        if subject_exclude in subject:
+                            success = False
+                            break
+                    if success is False:
+                        del subjects_and_callbacks[subject]
         except InitializingEventManagerError as e:
             error = f"App.event_registrar critical error: {str(e)}"
             raise InitializingEventManagerError(error)
@@ -239,15 +265,13 @@ def _start_tasks(self):
         tasks = asyncio.all_tasks(loop)
         for coro in self.tasks:
             if not asyncio.iscoroutinefunction(coro):
-                raise InitializingTaskError(
-                    "For asyncio app_strategy only coroutine tasks allowed"
-                )
+                raise InitializingTaskError("Only coroutine tasks allowed")
             loop.create_task(coro())
         for interval in self.interval_tasks:
             for coro in self.interval_tasks[interval]:
                 if not asyncio.iscoroutinefunction(coro):
                     raise InitializingIntervalTaskError(
-                        "For asyncio app_strategy only coroutine interval tasks allowed"
+                        "Only coroutine interval tasks allowed"
                     )
                 loop.create_task(coro())
         if self.http_server:
diff --git a/panini/managers.py b/panini/managers.py
index 9685bbd..46663f2 100644
--- a/panini/managers.py
+++ b/panini/managers.py
@@ -50,7 +50,7 @@ def _subscribe(self, subject, function):
             raise NotReadyError(
                 "Something wrong. NATS client should be connected first"
             )
-        self.subscribe_new_subject(subject, function)
+        self.subscribe_new_subject_sync(subject, function)
 
     @staticmethod
     def wrap_function_by_validator(function, validator):
@@ -239,13 +239,13 @@ def wrap_function_by_send_middleware(
                 def next_wrapper(subject: str, message, *args, **kwargs):
                     return single_middleware(subject, message, func, *args, **kwargs)
 
-                async def aio_next_wrapper(subject: str, message, *args, **kwargs):
+                async def async_next_wrapper(subject: str, message, *args, **kwargs):
                     return await single_middleware(
                         subject, message, func, *args, **kwargs
                     )
 
                 if asyncio.iscoroutinefunction(single_middleware):
-                    return aio_next_wrapper
+                    return async_next_wrapper
                 else:
                     return next_wrapper
 
@@ -255,11 +255,11 @@ def wrap_function_by_listen_middleware(
                 def next_wrapper(msg):
                     return single_middleware(msg, func)
 
-                async def aio_next_wrapper(msg):
+                async def async_next_wrapper(msg):
                     return await single_middleware(msg, func)
 
                 if asyncio.iscoroutinefunction(single_middleware):
-                    return aio_next_wrapper
+                    return async_next_wrapper
                 else:
                     return next_wrapper
 
@@ -309,14 +309,14 @@ def listen_wrapper(msg) -> Callable:
                     else:
                         return function_listen_request(msg)
 
-                async def aio_listen_wrapper(msg) -> Callable:
+                async def async_listen_wrapper(msg) -> Callable:
                     if msg.reply == "":
                         return await function_listen_publish(msg)
                     else:
                         return await function_listen_request(msg)
 
                 if asyncio.iscoroutinefunction(function):
-                    return aio_listen_wrapper
+                    return async_listen_wrapper
                 else:
                     return listen_wrapper
 
diff --git a/panini/middleware/debug_middleware.py b/panini/middleware/debug_middleware.py
new file mode 100644
index 0000000..3880119
--- /dev/null
+++ b/panini/middleware/debug_middleware.py
@@ -0,0 +1,42 @@
+from panini.middleware import Middleware
+from panini.utils.logger import get_logger
+
+
+class DebugMiddleware(Middleware):
+    def __init__(
+        self,
+        logger=None,
+        logger_name="panini",
+        log_level="debug",
+        use_send_any=True,
+        use_listen_any=True,
+    ):
+        self.logger = logger if logger is not None else get_logger(logger_name)
+        self.log_level = log_level.lower()
+        self.use_send_any = use_send_any
+        self.use_listen_any = use_listen_any
+
+    def _log(self, *args, **kwargs):
+        return getattr(self.logger, self.log_level)(*args, **kwargs)
+
+    async def send_any(self, subject: str, message, send_func, *args, **kwargs):
+        if not self.use_send_any:
+            return await send_func(subject, message, *args, **kwargs)
+
+        self._log(f"Sent to {subject} message {message}")
+        response = await send_func(subject, message, *args, **kwargs)
+        if response is not None:
+            self._log(f"Received response: {response}")
+
+        return response
+
+    async def listen_any(self, msg, callback):
+        if not self.use_listen_any:
+            return await callback(msg)
+
+        self._log(f"Listen to {msg.subject} with payload {msg.data}")
+        response = await callback(msg)
+        if response is not None:
+            self._log(f"Answered to it: {response}")
+
+        return response
diff --git a/panini/nats_client.py b/panini/nats_client.py
index 54aa4f2..93e17bc 100644
--- a/panini/nats_client.py
+++ b/panini/nats_client.py
@@ -1,3 +1,4 @@
+import typing
 import ujson
 import asyncio
 import threading
@@ -72,16 +73,14 @@ def __init__(
         self.reconnecting_time_wait = reconnecting_time_wait
         self.pending_bytes_limit = pending_bytes_limit
         self.ssid_map = {}
-        # publish function without middlewares
-        self.raw_publish = self.publish
 
         # inject send_middlewares
-        self.publish = _MiddlewareManager._wrap_function_by_middleware("publish")(
-            self.publish
-        )
-        self.request = _MiddlewareManager._wrap_function_by_middleware("request")(
-            self.request
-        )
+        self._publish_wrapped = _MiddlewareManager._wrap_function_by_middleware(
+            "publish"
+        )(self._publish)
+        self._request_wrapped = _MiddlewareManager._wrap_function_by_middleware(
+            "request"
+        )(self._request)
 
         self.loop = asyncio.get_event_loop()
         self.loop.run_until_complete(self._establish_connection())
@@ -104,23 +103,31 @@ async def _establish_connection(self):
             listen_subjects_callbacks = self.listen_subjects_callbacks
             for subject, callbacks in listen_subjects_callbacks.items():
                 for callback in callbacks:
-                    await self.aio_subscribe_new_subject(
-                        subject, callback, init_subscription=True
-                    )
+                    await self.subscribe_new_subject(
+                            subject, callback, init_subscription=True
+                        )
 
-    def subscribe_new_subject(self, subject: str, callback: CoroutineType):
-        self.loop.run_until_complete(self.aio_subscribe_new_subject(subject, callback))
+    def subscribe_new_subject_sync(self, subject: str, callback: CoroutineType, **kwargs):
+        self.loop.run_until_complete(self.subscribe_new_subject(subject, callback, **kwargs))
 
-    async def aio_subscribe_new_subject(
-        self, subject: str, callback: CoroutineType, init_subscription=False
+    async def subscribe_new_subject(
+        self,
+        subject: str,
+        callback: CoroutineType,
+        init_subscription=False,
+        is_async=False,
+        data_type=None
     ):
+        if data_type == None:
+            data_type = getattr(callback, 'data_type', 'json.loads')
         callback = _MiddlewareManager._wrap_function_by_middleware("listen")(callback)
-        wrapped_callback = _ReceivedMessageHandler(self.raw_publish, callback)
+        wrapped_callback = _ReceivedMessageHandler(self._publish, callback, data_type)
         ssid = await self.client.subscribe(
             subject,
             queue=self.queue,
             cb=wrapped_callback,
             pending_bytes_limit=self.pending_bytes_limit,
+            is_async=is_async,
         )
         if subject not in self.ssid_map:
             self.ssid_map[subject] = []
@@ -131,10 +138,10 @@ async def aio_subscribe_new_subject(
             self.listen_subjects_callbacks[subject].append(callback)
         return ssid
 
-    def unsubscribe_subject(self, subject: str):
-        self.loop.run_until_complete(self.aio_unsubscribe_subject(subject))
+    def unsubscribe_subject_sync(self, subject: str):
+        self.loop.run_until_complete(self.unsubscribe_subject(subject))
 
-    async def aio_unsubscribe_subject(self, subject: str):
+    async def unsubscribe_subject(self, subject: str):
         if subject not in self.ssid_map:
             raise Exception(f"Subject {subject} hasn't been subscribed")
         for ssid in self.ssid_map[subject]:
@@ -142,10 +149,10 @@ async def aio_unsubscribe_subject(self, subject: str):
         del self.ssid_map[subject]
         del self.listen_subjects_callbacks[subject]
 
-    def unsubscribe_ssid(self, ssid: int, subject: str = None):
-        self.loop.run_until_complete(self.aio_unsubscribe_ssid(ssid, subject))
+    def unsubscribe_ssid_sync(self, ssid: int, subject: str = None):
+        self.loop.run_until_complete(self.unsubscribe_ssid(ssid, subject))
 
-    async def aio_unsubscribe_ssid(self, ssid: int, subject: str = None):
+    async def unsubscribe_ssid(self, ssid: int, subject: str = None):
         if subject and subject not in self.ssid_map:
             raise Exception(f"Subject {subject} hasn't been subscribed")
         await self.client.unsubscribe(ssid)
@@ -181,10 +188,11 @@ def request_sync(
         message,
         timeout: int = 10,
         data_type: type or str = "json.dumps",
+        callback: typing.Callable = None,
     ):
         # asyncio.ensure_future(self.request(subject, message, timeout, data_type))
         return self.loop.run_until_complete(
-            self.request(subject, message, timeout, data_type)
+            self.request(subject, message, timeout, data_type, callback)
         )
 
     def request_from_another_thread_sync(
@@ -222,7 +230,7 @@ def fut_finished_cb(_):
 
     @staticmethod
     def format_message_data_type(message, data_type):
-        if type(message) is dict and data_type == "json.dumps":
+        if type(message) in [dict, list] and data_type == "json.dumps":
             message = ujson.dumps(message)
             message = message.encode()
         elif type(message) is str and data_type is str:
@@ -236,7 +244,7 @@ def format_message_data_type(message, data_type):
 
         return message
 
-    async def publish(
+    async def _publish(
         self,
         subject: str,
         message,
@@ -253,14 +261,33 @@ async def publish(
         if force:
             await self.client.flush()
 
-    async def request(
+    async def publish(
+        self,
+        subject: str,
+        message,
+        reply_to: str = None,
+        force: bool = False,
+        data_type: type or str = "json.dumps",
+    ):
+        return await self._publish_wrapped(
+            subject=subject,
+            message=message,
+            reply_to=reply_to,
+            force=force,
+            data_type=data_type,
+        )
+
+    async def _request(
         self,
         subject: str,
         message,
         timeout: int = 10,
         data_type: type or str = "json.dumps",
+        callback: typing.Callable = None,
     ):
         message = self.format_message_data_type(message, data_type)
+        if callback is not None:
+            return await self.client.request(subject, message, cb=callback)
         response = await self.client.request(subject, message, timeout=timeout)
         response = response.data
         if data_type == "json.dumps":
@@ -269,10 +296,26 @@ async def request(
             response = response.decode()
         return response
 
-    def disconnect(self):
-        self.loop.run_until_complete(self.aio_disconnect())
+    async def request(
+        self,
+        subject: str,
+        message,
+        timeout: int = 10,
+        data_type: type or str = "json.dumps",
+        callback: typing.Callable = None,
+    ):
+        return await self._request_wrapped(
+            subject=subject,
+            message=message,
+            timeout=timeout,
+            data_type=data_type,
+            callback=callback,
+        )
+
+    def disconnect_sync(self):
+        self.loop.run_until_complete(self.disconnect())
 
-    async def aio_disconnect(self):
+    async def disconnect(self):
         await self.client.drain()
         self.log.warning("Disconnected")
 
@@ -284,10 +327,10 @@ def check_connection(self):
 
 
 class _ReceivedMessageHandler:
-    def __init__(self, publish_func, cb):
+    def __init__(self, publish_func, cb, data_type):
         self.publish_func = publish_func
         self.cb = cb
-        self.data_type = getattr(cb, "data_type", "json.loads")
+        self.data_type = data_type
         self.cb_is_async = asyncio.iscoroutinefunction(cb)
 
     def __call__(self, msg):
diff --git a/panini/utils/logger.py b/panini/utils/logger.py
index b5b1d40..9032316 100644
--- a/panini/utils/logger.py
+++ b/panini/utils/logger.py
@@ -12,7 +12,7 @@
 
 
 # Decorator for check, if logger exist
-def check_logger(func):
+def check_logger_connected(func):
     def wrapper(self, msg, **extra):
         if self.logger is None:
             raise Exception("Logger hasn't been connected")
@@ -30,23 +30,23 @@ class Logger:
     def __init__(self, logger: logging.Logger):
         self.logger = logger
 
-    @check_logger
+    @check_logger_connected
     def debug(self, msg, **extra):
         self.logger.debug(msg, extra={"extra": extra})
 
-    @check_logger
+    @check_logger_connected
     def info(self, msg, **extra):
         self.logger.info(msg, extra={"extra": extra})
 
-    @check_logger
+    @check_logger_connected
     def warning(self, msg, **extra):
         self.logger.warning(msg, extra={"extra": extra})
 
-    @check_logger
+    @check_logger_connected
     def error(self, msg, **extra):
         self.logger.error(msg, extra={"extra": extra})
 
-    @check_logger
+    @check_logger_connected
     def exception(self, msg, **extra):
         self.logger.exception(msg, extra={"extra": extra})
 
diff --git a/requirements/defaults.txt b/requirements/defaults.txt
index 7243e08..2b935ad 100644
--- a/requirements/defaults.txt
+++ b/requirements/defaults.txt
@@ -7,6 +7,7 @@ six==1.15.0
 nats-python==0.8.0
 nest-asyncio==1.5.1
 pytest==6.2.1
+pytest-asyncio==0.15.1
 prometheus-client==0.9.0
 python-json-logger==2.0.1
 yarl==1.6.1
diff --git a/setup.py b/setup.py
index 2d4e6b2..8d5421d 100644
--- a/setup.py
+++ b/setup.py
@@ -19,7 +19,7 @@
 
 setup(
     name="panini",
-    version="0.3.0",
+    version="0.5.0",
     description="A python messaging framework for microservices based on NATS",
     long_description=long_description,
     long_description_content_type="text/x-rst",


Test patch:
diff --git a/panini/async_test_client.py b/panini/async_test_client.py
new file mode 100644
index 0000000..f22ae81
--- /dev/null
+++ b/panini/async_test_client.py
@@ -0,0 +1,390 @@
+import asyncio
+import os
+import shutil
+import json
+import random
+import time
+import typing
+from urllib.parse import urljoin
+
+import requests
+import websocket
+from nats.aio.client import Client as NATS
+
+from .exceptions import TestClientError
+from .utils.helper import start_process
+from panini.nats_client import Msg
+
+
+# Annotations for `Session.request()`
+Cookies = typing.Union[
+    typing.MutableMapping[str, str], requests.cookies.RequestsCookieJar
+]
+Params = typing.Union[bytes, typing.MutableMapping[str, str]]
+DataType = typing.Union[bytes, typing.MutableMapping[str, str], typing.IO]
+TimeOut = typing.Union[float, typing.Tuple[float, float]]
+FileType = typing.MutableMapping[str, typing.IO]
+AuthType = typing.Union[
+    typing.Tuple[str, str],
+    requests.auth.AuthBase,
+    typing.Callable[[requests.Request], requests.Request],
+]
+
+
+class HTTPSessionTestClient(requests.Session):
+    __test__ = False
+
+    def __init__(self, base_url: str = "http://127.0.0.1:8080"):
+        super(HTTPSessionTestClient, self).__init__()
+        self.base_url = base_url
+
+    def request(  # type: ignore
+        self,
+        method: str,
+        url: str,
+        params: Params = None,
+        data: DataType = None,
+        headers: typing.MutableMapping[str, str] = None,
+        cookies: Cookies = None,
+        files: FileType = None,
+        auth: AuthType = None,
+        timeout: TimeOut = None,
+        allow_redirects: bool = None,
+        proxies: typing.MutableMapping[str, str] = None,
+        hooks: typing.Any = None,
+        stream: bool = None,
+        verify: typing.Union[bool, str] = None,
+        cert: typing.Union[str, typing.Tuple[str, str]] = None,
+        json: typing.Any = None,
+    ) -> requests.Response:
+        url = urljoin(self.base_url, url)
+        return super().request(
+            method,
+            url,
+            params=params,
+            data=data,
+            headers=headers,
+            cookies=cookies,
+            files=files,
+            auth=auth,
+            timeout=timeout,
+            allow_redirects=allow_redirects,
+            proxies=proxies,
+            hooks=hooks,
+            stream=stream,
+            verify=verify,
+            cert=cert,
+            json=json,
+        )
+
+
+def get_logger_files_path(folder: str = "test_logs", remove_if_exist: bool = False):
+    testing_directory_path = os.getcwd()
+    testing_logs_directory_path = os.path.join(testing_directory_path, folder)
+    if remove_if_exist:
+        if os.path.exists(testing_logs_directory_path):
+            shutil.rmtree(testing_logs_directory_path)
+
+    return testing_logs_directory_path
+
+
+def is_subject_matches_pattern(subject: str, pattern: str) -> bool:
+    subject_parts = subject.split(".")
+    pattern_parts = pattern.split(".")
+    if ">" in pattern:
+        assert pattern[-1] == ">", "> must can be only the last element of subject!"
+        gt_position = pattern_parts.index(">")
+        return is_subject_matches_pattern(
+            ".".join(subject_parts[:gt_position]),
+            ".".join(pattern_parts[:gt_position]),
+        )
+
+    if ">" in subject_parts:
+        return False
+
+    if len(subject_parts) != len(pattern_parts):
+        return False
+
+    for index, part in enumerate(pattern_parts):
+        if part == "*":
+            pass
+        elif part != subject_parts[index]:
+            return False
+
+    return True
+
+
+class AsyncTestClient:
+    __test__ = False
+
+    def __init__(
+        self,
+        run_panini: typing.Callable = None,
+        run_panini_args: list = None,
+        run_panini_kwargs: dict = None,
+        run_panini_timeout: float = 5,
+        panini_service_name: str = "*",
+        panini_client_id: str = "*",
+        logger_files_path: str = "test_logs",
+        use_web_server: bool = False,
+        use_web_socket: bool = False,
+        base_web_server_url: str = "http://127.0.0.1:8080",
+        nats_host: str = "127.0.0.1",
+        nats_port: int = 4222,
+        name: str = "__".join(
+            [
+                "test_client",
+                str(random.randint(1, 10000000)),
+                str(random.randint(1, 10000000)),
+            ]
+        ),
+        listen_subjects_callbacks: dict = None,
+    ):
+        self.run_panini = run_panini
+        self.run_panini_args = run_panini_args or []
+        self.run_panini_kwargs = run_panini_kwargs or {}
+        self.run_panini_timeout = run_panini_timeout
+        self.listen_subjects_callbacks = listen_subjects_callbacks or {}
+        self.panini_service_name = panini_service_name
+        self.panini_client_id = panini_client_id
+        self.logger_files_path = logger_files_path
+        self.base_web_server_url = base_web_server_url
+        self.use_web_server = use_web_server
+        self.use_web_socket = use_web_socket
+        self.name = name
+        self.nats_client = NATS()
+        self.nats_client.msg_class = Msg
+        self.nats_host = nats_host
+        self.nats_port = nats_port
+        self._listen_subjects_count_calls = {}
+
+        if use_web_server:
+            self.http_session = HTTPSessionTestClient(base_url=base_web_server_url)
+
+        if use_web_socket:
+            self.websocket_session = websocket.WebSocket()
+
+        self.panini_process = None
+
+    @staticmethod
+    def _dict_to_bytes(message: dict) -> bytes:
+        return json.dumps(message).encode("utf-8")
+
+    @staticmethod
+    def _bytes_to_dict(payload: bytes) -> dict:
+        return json.loads(payload)
+
+    @staticmethod
+    def wrap_run_panini(
+        run_panini,
+        run_panini_args: list,
+        run_panini_kwargs: dict,
+        logger_files_path: str,
+        use_error_middleware: bool,
+    ):
+        from .utils.logger import get_logger
+
+        test_logger = get_logger("panini")
+        # set the panini testing data in os.environ
+        os.environ["PANINI_TEST_MODE"] = "true"
+        os.environ["PANINI_TEST_MODE_USE_ERROR_MIDDLEWARE"] = (
+            "true" if use_error_middleware else "false"
+        )
+        testing_logger_files_path = (
+            get_logger_files_path(logger_files_path)
+            if not os.path.isabs(logger_files_path)
+            else logger_files_path
+        )
+
+        os.environ["PANINI_TEST_LOGGER_FILES_PATH"] = testing_logger_files_path
+
+        loop = asyncio.new_event_loop()
+        asyncio.set_event_loop(loop)
+        try:
+            run_panini(*run_panini_args, **run_panini_kwargs)
+        except Exception as e:
+            test_logger.exception(f"Run panini error: {e}")
+
+    async def start(self, is_daemon: bool = True, use_error_middleware=True):
+        await self.nats_client.connect(
+            f"{self.nats_host}:{self.nats_port}", name=self.name
+        )
+        for subject, callback in self.listen_subjects_callbacks.items():
+            await self.nats_client.subscribe(subject, cb=callback)
+
+        if self.run_panini is not None:
+
+            panini_started_future = asyncio.Future()
+
+            def panini_started(msg):
+                panini_started_future.set_result(msg)
+
+            sid = await self.nats_client.subscribe(
+                f"panini_events.{self.panini_service_name}.{self.panini_client_id}.started",
+                cb=panini_started,
+            )
+            await self.nats_client.auto_unsubscribe(sid, 1)
+
+            self.panini_process = start_process(
+                self.wrap_run_panini,
+                args=(
+                    self.run_panini,
+                    self.run_panini_args,
+                    self.run_panini_kwargs,
+                    self.logger_files_path,
+                    use_error_middleware,
+                ),
+                daemon=is_daemon,
+            )
+
+            try:
+                await asyncio.wait_for(panini_started_future, self.run_panini_timeout)
+            except Exception:
+                raise TestClientError(
+                    "TestClient was waiting panini to start, but panini does not started"
+                )
+
+            if self.use_web_server:
+                pass  # TODO: understand, why don't we need to wait for web_server
+
+        return self
+
+    async def stop(self):
+        await self.nats_client.drain()
+        await self.nats_client.close()
+
+        if self.panini_process is not None:
+            self.panini_process.kill()
+
+        if hasattr(self, "http_session"):
+            self.http_session.close()
+
+        if hasattr(self, "websocket_session"):
+            self.websocket_session.close()
+
+    async def publish(self, subject: str, message: dict, reply_to: str = "") -> None:
+        message = self._dict_to_bytes(message)
+        if reply_to is not None:
+            await self.nats_client.publish_request(subject, reply_to, message)
+        else:
+            await self.nats_client.publish(subject, message)
+
+    async def request(self, subject: str, message: dict, timeout: int = 1) -> dict:
+        response = await self.nats_client.request(
+            subject=subject, payload=self._dict_to_bytes(message), timeout=timeout
+        )
+        return self._bytes_to_dict(response.data)
+
+    async def subscribe(
+        self,
+        subject: str,
+        callback: typing.Callable,
+        queue: str = "",
+        max_messages: typing.Optional[int] = 0,
+    ):
+        return await self.nats_client.subscribe(
+            subject=subject,
+            cb=callback,
+            queue=queue,
+            max_msgs=max_messages,
+        )
+
+    def total_count_subject_calls(self, count_calls: dict = None):
+        if count_calls is None:
+            count_calls = self._listen_subjects_count_calls
+        return sum(count_calls.values())
+
+    def count_subject_calls(self, subject: str, count_calls: dict = None):
+        """Count calls for specific subject (this subject is used as pattern for subject - supports * and >)"""
+        if count_calls is None:
+            count_calls = self._listen_subjects_count_calls
+        res_count = 0
+        for listen_subject, count in count_calls.items():
+            if is_subject_matches_pattern(listen_subject, subject):
+                res_count += count
+
+        return res_count
+
+    def clear_listen_subjects_count_calls(self):
+        """Clean up all counts for subject called in AsyncTestClient"""
+        self._listen_subjects_count_calls = {}
+
+    async def wait(
+        self,
+        count: int = 0,
+        timeout: float = 1,
+        subject: str = None,
+        subjects: dict = None,
+    ) -> None:
+        """
+        Waits for test client subjects to be triggered & handled.
+        count: how much calls wait
+        timeout: maximum seconds to wait
+        subject: which subject to wait
+        subjects: dict of subjects, with subject as key and count_to_wait as value
+        """
+        start_time = time.time()
+
+        while time.time() - start_time < timeout:
+            if subject is not None:
+                if self.count_subject_calls(subject) >= count:
+                    return
+            elif subjects is not None:
+                if all(
+                    [
+                        self.count_subject_calls(subj) >= count_call
+                        for subj, count_call in subjects.items()
+                    ]
+                ):
+                    return
+            elif self.total_count_subject_calls() >= count:
+                return
+            await asyncio.sleep(0)
+
+        # wait was not successful
+        if subject is not None:
+            report_msg = f"subject: {subject} was called {self.count_subject_calls(subject)} times"
+        elif subjects is not None:
+            subjects_calls = {subj: self.count_subject_calls(subj) for subj in subjects}
+            report_msg = f"subjects were called {subjects_calls} times"
+        else:
+            report_msg = f"total_count_subject_calls={self.total_count_subject_calls()}"
+        raise asyncio.TimeoutError(
+            f"Timeout while waiting for listen_subjects to be called! Params - count: {count}, timeout: {timeout}, subject: {subject}, subjects: {subjects}. Actual state: {report_msg}"
+        )
+
+    def listen(self, subject: str):
+        def decorator(func):
+            assert isinstance(subject, str), "Subject must be only in str format"
+
+            async def wrapper(incoming_message):
+                assert isinstance(
+                    incoming_message, Msg
+                ), "Incoming message must be instance of Msg!"
+                incoming_message_data = self._bytes_to_dict(incoming_message.data)
+
+                msg = Msg(
+                    subject=incoming_message.subject,
+                    data=incoming_message_data,
+                    reply=incoming_message.reply,
+                    sid=incoming_message.sid,
+                )
+                if asyncio.iscoroutinefunction(func):
+                    wrapper_response = await func(msg)
+                else:
+                    wrapper_response = func(msg)
+                if wrapper_response is not None and incoming_message.reply != "":
+                    await self.nats_client.publish(
+                        subject=incoming_message.reply,
+                        payload=self._dict_to_bytes(wrapper_response),
+                    )
+
+                self._listen_subjects_count_calls[msg.subject] = (
+                    self._listen_subjects_count_calls.get(msg.subject, 0) + 1
+                )
+
+            self.listen_subjects_callbacks[subject] = wrapper
+
+            return wrapper
+
+        return decorator
diff --git a/panini/test_client.py b/panini/test_client.py
index 1c4ae8d..d8bbf24 100644
--- a/panini/test_client.py
+++ b/panini/test_client.py
@@ -1,6 +1,6 @@
 import os
 import shutil
-import time
+import threading
 import json
 import random
 import typing
@@ -11,7 +11,7 @@
 from pynats import NATSClient, NATSMessage
 
 from .exceptions import TestClientError
-from .utils.helper import start_process
+from .utils.helper import start_process, start_thread
 from panini.nats_client import Msg
 
 
@@ -92,7 +92,7 @@ class TestClient:
 
     def __init__(
         self,
-        run_panini: typing.Callable = lambda *args, **kwargs: None,
+        run_panini: typing.Callable = None,
         run_panini_args: list = None,
         run_panini_kwargs: dict = None,
         panini_service_name: str = "*",
@@ -103,6 +103,7 @@ def __init__(
         base_web_server_url: str = "http://127.0.0.1:8080",
         base_nats_url: str = "nats://127.0.0.1:4222",
         socket_timeout: int = 5,
+        listener_socket_timeout: int = 1,
         auto_reconnect: bool = True,
         name: str = "__".join(
             [
@@ -111,10 +112,12 @@ def __init__(
                 str(random.randint(1, 10000000)),
             ]
         ),
+        _subscribed_subjects: [str] = None,
     ):
         self.run_panini = run_panini
         self.run_panini_args = run_panini_args or []
         self.run_panini_kwargs = run_panini_kwargs or {}
+        self._subscribed_subjects = _subscribed_subjects or []
         self.panini_service_name = panini_service_name
         self.panini_client_id = panini_client_id
         self.logger_files_path = logger_files_path
@@ -123,10 +126,15 @@ def __init__(
         self.use_web_socket = use_web_socket
         self.name = name
         self.base_nats_url = base_nats_url
-        self.socket_timeout = socket_timeout
         self.auto_reconnect = auto_reconnect
-        self.nats_client = self.create_nats_client()
-        self.nats_client.connect()
+        self.nats_client_sender = self.create_nats_client("sender", socket_timeout)
+        self.nats_client_listener = self.create_nats_client(
+            "listener", listener_socket_timeout
+        )
+        self.nats_client_sender.connect()
+        self.nats_client_listener.connect()
+        self.nats_client_listener_thread = None
+        self.nats_client_listener_thread_stop_event = threading.Event()
 
         if use_web_server:
             self.http_session = HTTPSessionTestClient(base_url=base_web_server_url)
@@ -136,11 +144,11 @@ def __init__(
 
         self.panini_process = None
 
-    def create_nats_client(self) -> NATSClient:
+    def create_nats_client(self, suffix: str, nats_timeout) -> NATSClient:
         return NATSClient(
             url=self.base_nats_url,
-            name=self.name,
-            socket_timeout=self.socket_timeout,
+            name=self.name + suffix,
+            socket_timeout=nats_timeout,
         )
 
     @staticmethod
@@ -157,12 +165,16 @@ def wrap_run_panini(
         run_panini_args: list,
         run_panini_kwargs: dict,
         logger_files_path: str,
+        use_error_middleware: bool,
     ):
         from .utils.logger import get_logger
 
         test_logger = get_logger("panini")
         # set the panini testing data in os.environ
         os.environ["PANINI_TEST_MODE"] = "true"
+        os.environ["PANINI_TEST_MODE_USE_ERROR_MIDDLEWARE"] = (
+            "true" if use_error_middleware else "false"
+        )
         testing_logger_files_path = (
             get_logger_files_path(logger_files_path)
             if not os.path.isabs(logger_files_path)
@@ -175,46 +187,72 @@ def wrap_run_panini(
         except Exception as e:
             test_logger.exception(f"Run panini error: {e}")
 
-    def start(self, is_sync: bool = False, is_daemon: bool = None):
-        if is_daemon is None:
-            is_daemon = False if is_sync else True
-
-        @self.listen(
-            f"panini_events.{self.panini_service_name}.{self.panini_client_id}.started"
-        )
-        def panini_started(msg):
-            pass
-
-        self.panini_process = start_process(
-            self.wrap_run_panini,
-            args=(
-                self.run_panini,
-                self.run_panini_args,
-                self.run_panini_kwargs,
-                self.logger_files_path,
-            ),
-            daemon=is_daemon,
-        )
+    def start(
+        self,
+        is_daemon: bool = True,
+        do_always_listen: bool = True,
+        use_error_middleware=True,
+    ):
+        if do_always_listen and len(self._subscribed_subjects) > 0:
+
+            def nats_listener_worker(stop_event):
+                try:
+                    while not stop_event.wait(1):
+                        self.nats_client_listener.wait()
+                except Exception:
+                    pass
+
+            # Run nats-listener process
+            self.nats_client_listener_thread = start_thread(
+                nats_listener_worker,
+                args=[self.nats_client_listener_thread_stop_event],
+                daemon=True,
+            )
+
+        if self.run_panini is not None:
+
+            def panini_started(msg):
+                pass
+
+            sub = self.nats_client_sender.subscribe(
+                f"panini_events.{self.panini_service_name}.{self.panini_client_id}.started",
+                callback=panini_started,
+                max_messages=1,
+            )
+            self.nats_client_sender.auto_unsubscribe(sub)
+
+            self.panini_process = start_process(
+                self.wrap_run_panini,
+                args=(
+                    self.run_panini,
+                    self.run_panini_args,
+                    self.run_panini_kwargs,
+                    self.logger_files_path,
+                    use_error_middleware,
+                ),
+                daemon=is_daemon,
+            )
 
-        if is_sync:
-            time.sleep(5)
-        else:
             try:
-                self.wait(1)  # wait for panini to start
+                self.nats_client_sender.wait(count=1)  # wait for panini to start
             except OSError:
                 raise TestClientError(
                     "TestClient was waiting panini to start, but panini does not started"
                 )
 
-        if self.use_web_server:
-            pass  # TODO: understand, why don't we need to wait for web_server
+            if self.use_web_server:
+                pass  # TODO: understand, why don't we need to wait for web_server
 
         return self
 
     def stop(self):
-        self.nats_client.close()
+        self.nats_client_listener.close()
+        self.nats_client_sender.close()
+        self.nats_client_listener_thread_stop_event.set()
+
         if self.panini_process:
             self.panini_process.kill()
+
         if hasattr(self, "http_session"):
             self.http_session.close()
 
@@ -222,19 +260,24 @@ def stop(self):
             self.websocket_session.close()
 
     def publish(self, subject: str, message: dict, reply: str = "") -> None:
-        self.nats_client.publish(
+        self.nats_client_sender.publish(
             subject=subject, payload=self._dict_to_bytes(message), reply=reply
         )
 
-    def request(self, subject: str, message: dict) -> dict:
-        if self.auto_reconnect:
+    def reconnect(self):
+        if (
+            self.auto_reconnect
+        ):  # reconnects nats_client for next call, if it was disconnected earlier
             try:
-                self.nats_client.ping()
+                self.nats_client_sender.ping()
             except Exception:
-                self.nats_client.reconnect()
+                self.nats_client_sender.reconnect()
+
+    def request(self, subject: str, message: dict) -> dict:
+        self.reconnect()
 
         return self._bytes_to_dict(
-            self.nats_client.request(
+            self.nats_client_sender.request(
                 subject=subject, payload=self._dict_to_bytes(message)
             ).payload
         )
@@ -246,7 +289,8 @@ def subscribe(
         queue: str = "",
         max_messages: typing.Optional[int] = None,
     ):
-        return self.nats_client.subscribe(
+        self._subscribed_subjects.append(subject)
+        return self.nats_client_listener.subscribe(
             subject=subject,
             callback=callback,
             queue=queue,
@@ -254,32 +298,34 @@ def subscribe(
         )
 
     def wait(self, count: int) -> None:
-        self.nats_client.wait(count=count)
+        if self.nats_client_listener_thread is not None:
+            raise TestClientError(
+                "You can't use client.wait, "
+                "if you don't have any @client.listen functions "
+                "or you don't use do_always_listen=False!"
+            )
+        self.nats_client_listener.wait(count=count)
 
     def listen(self, subject: str):
         def decorator(func):
-            assert isinstance(subject, str)
+            assert isinstance(subject, str), "Subject must be only in str format"
+
+            def wrapper(incoming_message):
+                assert isinstance(incoming_message, NATSMessage)
+                incoming_message_data = self._bytes_to_dict(incoming_message.payload)
 
-            def wrapper(incoming_response):
-                assert isinstance(incoming_response, NATSMessage)
                 msg = Msg(
-                    subject=incoming_response.subject,
-                    data=self._bytes_to_dict(incoming_response.payload),
-                    reply=incoming_response.reply,
-                    sid=incoming_response.sid,
+                    subject=incoming_message.subject,
+                    data=incoming_message_data,
+                    reply=incoming_message.reply,
+                    sid=incoming_message.sid,
                 )
                 wrapper_response = func(msg)
-                if wrapper_response is not None and incoming_response.reply != "":
-                    self.publish(
-                        subject=incoming_response.reply, message=wrapper_response
+                if wrapper_response is not None and incoming_message.reply != "":
+                    self.nats_client_listener.publish(
+                        subject=incoming_message.reply,
+                        payload=self._dict_to_bytes(wrapper_response),
                     )
-                    try:
-                        self.wait(count=1)
-                    except OSError:
-                        raise TestClientError(
-                            f"TestClient listen subject: {incoming_response.subject},"
-                            f"Response was sent, but it was not correctly handled"
-                        )
 
             self.subscribe(subject, wrapper)
 
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..be46033
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,4 @@
+[pytest]
+log_cli = true
+filterwarnings =
+    ignore::DeprecationWarning
\ No newline at end of file
diff --git a/tests/async_test_client/__init__.py b/tests/async_test_client/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/async_test_client/conftest.py b/tests/async_test_client/conftest.py
new file mode 100644
index 0000000..d881010
--- /dev/null
+++ b/tests/async_test_client/conftest.py
@@ -0,0 +1,10 @@
+import asyncio
+
+import pytest
+
+
+@pytest.fixture(scope="module")
+def event_loop():
+    loop = asyncio.get_event_loop()
+    yield loop
+    loop.close()
diff --git a/tests/async_test_client/separate_file/main.py b/tests/async_test_client/separate_file/main.py
new file mode 100644
index 0000000..17581ee
--- /dev/null
+++ b/tests/async_test_client/separate_file/main.py
@@ -0,0 +1,20 @@
+from panini import app as panini_app
+
+app = panini_app.App(
+    service_name="separate_file",
+    host="127.0.0.1",
+    port=4222,
+    logger_in_separate_process=False,
+)
+
+
+@app.listen("separate_file.async_test_client.listen_request")
+async def listen_request(msg):
+    return {
+        "success": True,
+        "message": "separate_file.async_test_client.listen_request",
+    }
+
+
+if __name__ == "__main__":
+    app.start()
diff --git a/tests/async_test_client/separate_file/test_main.py b/tests/async_test_client/separate_file/test_main.py
new file mode 100644
index 0000000..873ba94
--- /dev/null
+++ b/tests/async_test_client/separate_file/test_main.py
@@ -0,0 +1,24 @@
+import pytest
+
+from panini.async_test_client import AsyncTestClient
+
+
+def run_panini():
+    from tests.async_test_client.separate_file.main import app
+
+    app.start()
+
+
+@pytest.fixture
+async def client():
+    client = await AsyncTestClient(run_panini).start()
+    yield client
+    await client.stop()
+
+
+@pytest.mark.asyncio
+async def test_request(client: AsyncTestClient):
+    subject = "separate_file.async_test_client.listen_request"
+    response = await client.request(subject, {})
+    assert response["success"] is True
+    assert response["message"] == subject
diff --git a/tests/async_test_client/test_error_handling.py b/tests/async_test_client/test_error_handling.py
new file mode 100644
index 0000000..de1bb10
--- /dev/null
+++ b/tests/async_test_client/test_error_handling.py
@@ -0,0 +1,35 @@
+import asyncio
+
+import pytest
+
+from panini.async_test_client import AsyncTestClient
+from panini import app as panini_app
+
+
+def run_panini():
+    app = panini_app.App(
+        service_name="async_test_client_test_error_handling",
+        host="127.0.0.1",
+        port=4222,
+    )
+
+    @app.listen("async_test_client.test_error_handling")
+    async def listen(msg):
+        error = 1 // 0
+        return {"success": False if error else True}
+
+    app.start()
+
+
+@pytest.fixture
+async def client():
+    client = await AsyncTestClient(run_panini=run_panini).start()
+    yield client
+    await client.stop()
+
+
+@pytest.mark.asyncio
+async def test_error(client):
+    subject = "async_test_client.test_error_handling"
+    with pytest.raises(asyncio.TimeoutError):
+        await client.request(subject, {})
diff --git a/tests/async_test_client/test_is_subject_matches_pattern.py b/tests/async_test_client/test_is_subject_matches_pattern.py
new file mode 100644
index 0000000..efd367a
--- /dev/null
+++ b/tests/async_test_client/test_is_subject_matches_pattern.py
@@ -0,0 +1,14 @@
+from panini.async_test_client import is_subject_matches_pattern
+
+
+def test_is_subject_matches_pattern():
+    assert is_subject_matches_pattern("asdf.>", "asdf.>") is True
+    assert is_subject_matches_pattern("asdf.>.1234.*", "asdf.>") is True
+    assert is_subject_matches_pattern("asdf.>.1234.*", "asdf.*") is False
+    assert is_subject_matches_pattern("asdf.*.*.*", "asdf.*") is False
+    assert is_subject_matches_pattern("asdf.*.*.*", "asdf.*.*.*") is True
+    assert is_subject_matches_pattern("asdf.*.asdf.*", "asdf.*.*.*") is True
+    assert is_subject_matches_pattern("asdf.*.*.*", "asdf.*.asdf.*") is False
+    assert (
+        is_subject_matches_pattern("asdf.asdf.asdf.asdf", "asdf.asdf.asdf.asdf") is True
+    )
diff --git a/tests/async_test_client/test_listen.py b/tests/async_test_client/test_listen.py
new file mode 100644
index 0000000..2149664
--- /dev/null
+++ b/tests/async_test_client/test_listen.py
@@ -0,0 +1,41 @@
+import asyncio
+
+import pytest
+
+from panini.async_test_client import AsyncTestClient
+
+
+@pytest.fixture
+def future():
+    return asyncio.Future()
+
+
+@pytest.fixture
+async def client(future):
+    client = AsyncTestClient()
+
+    @client.listen("async_test_client.*")
+    async def listen(msg):
+        future.set_result({"listen": "success", "subject": msg.subject})
+
+    await client.start()
+    yield client
+    await client.stop()
+
+
+@pytest.mark.asyncio
+async def test_listen(client, future):
+    subject = "async_test_client.test_listen"
+    await client.publish(subject, {})
+    msg = await asyncio.wait_for(future, 1)
+    assert msg["listen"] == "success"
+    assert msg["subject"] == subject
+
+
+@pytest.mark.asyncio
+async def test_another_listen(client, future):
+    subject = "async_test_client.test_another_listen"
+    await client.publish(subject, {})
+    msg = await asyncio.wait_for(future, 1)
+    assert msg["listen"] == "success"
+    assert msg["subject"] == subject
diff --git a/tests/async_test_client/test_panini.py b/tests/async_test_client/test_panini.py
new file mode 100644
index 0000000..6182efe
--- /dev/null
+++ b/tests/async_test_client/test_panini.py
@@ -0,0 +1,51 @@
+import pytest
+
+from panini.async_test_client import AsyncTestClient
+from panini import app as panini_app
+
+
+def run_panini():
+    app = panini_app.App(
+        service_name="async_test_client_test_panini",
+        host="127.0.0.1",
+        port=4222,
+    )
+
+    @app.listen("async_test_client.test_panini")
+    async def listen(msg):
+        helper_subject = "async_test_client.test_panini_helper"
+        response = await app.request(helper_subject, {})
+        assert response["success"] is True
+        assert response["subject"] == helper_subject
+        return {"success": True, "subject": msg.subject}
+
+    app.start()
+
+
+@pytest.fixture(scope="module")
+async def client():
+    client = AsyncTestClient(run_panini=run_panini)
+
+    @client.listen("async_test_client.test_panini_helper")
+    async def listen(msg):
+        return {"success": True, "subject": msg.subject}
+
+    await client.start()
+    yield client
+    await client.stop()
+
+
+@pytest.mark.asyncio
+async def test_listen(client):
+    subject = "async_test_client.test_panini"
+    response = await client.request(subject, {})
+    assert response["success"] is True
+    assert response["subject"] == subject
+
+
+@pytest.mark.asyncio
+async def test_second(client):
+    subject = "async_test_client.test_panini"
+    response = await client.request(subject, {})
+    assert response["success"] is True
+    assert response["subject"] == subject
diff --git a/tests/async_test_client/test_panini2.py b/tests/async_test_client/test_panini2.py
new file mode 100644
index 0000000..9469784
--- /dev/null
+++ b/tests/async_test_client/test_panini2.py
@@ -0,0 +1,65 @@
+import pytest
+
+from panini.async_test_client import AsyncTestClient
+from panini import app as panini_app
+
+
+def run_panini():
+    app = panini_app.App(
+        service_name="async_test_client_test_panini",
+        host="127.0.0.1",
+        port=4222,
+    )
+
+    @app.task()
+    async def send():
+        await app.publish("async_test_client.test_panini_listener", {"data": "test"})
+
+    @app.listen("async_test_client.test_panini")
+    async def listen(msg):
+        helper_subject = "async_test_client.test_panini_helper"
+        response = await app.request(helper_subject, {})
+        assert response["success"] is True
+        assert response["subject"] == helper_subject
+        return {"success": True, "subject": msg.subject}
+
+    app.start()
+
+
+@pytest.fixture
+async def client():
+    client = AsyncTestClient(run_panini=run_panini)
+
+    @client.listen("async_test_client.test_panini_helper")
+    async def listen(msg):
+        return {"success": True, "subject": msg.subject}
+
+    @client.listen("async_test_client.test_panini_listener")
+    async def listen(msg):
+        pass
+
+    await client.start()
+    yield client
+    await client.stop()
+
+
+@pytest.mark.asyncio
+async def test_listen(client):
+    subject = "async_test_client.test_panini"
+    response = await client.request(subject, {})
+    assert response["success"] is True
+    assert response["subject"] == subject
+
+
+@pytest.mark.asyncio
+async def test_second(client):
+    subject = "async_test_client.test_panini"
+    for _ in range(10):
+        response = await client.request(subject, {})
+        assert response["success"] is True
+        assert response["subject"] == subject
+
+
+@pytest.mark.asyncio
+async def test_wait(client: AsyncTestClient):
+    await client.wait(count=1, subject="async_test_client.test_panini_listener")
diff --git a/tests/async_test_client/test_request.py b/tests/async_test_client/test_request.py
new file mode 100644
index 0000000..8b829dd
--- /dev/null
+++ b/tests/async_test_client/test_request.py
@@ -0,0 +1,24 @@
+import pytest
+
+from panini.async_test_client import AsyncTestClient
+
+
+@pytest.fixture
+async def client():
+    client = AsyncTestClient()
+
+    @client.listen("async_test_client.test_request")
+    async def listen(msg):
+        return {"listen": "success", "subject": msg.subject}
+
+    await client.start()
+    yield client
+    await client.stop()
+
+
+@pytest.mark.asyncio
+async def test_request(client: AsyncTestClient):
+    subject = "async_test_client.test_request"
+    response = await client.request(subject, {})
+    assert response["listen"] == "success"
+    assert response["subject"] == subject
diff --git a/tests/async_test_client/test_wait.py b/tests/async_test_client/test_wait.py
new file mode 100644
index 0000000..8b030fc
--- /dev/null
+++ b/tests/async_test_client/test_wait.py
@@ -0,0 +1,68 @@
+import pytest
+
+from panini.async_test_client import AsyncTestClient
+
+
+counter = 0
+second_counter = 0
+
+
+@pytest.fixture
+async def client():
+    global counter, second_counter
+    counter = 0
+    second_counter = 0
+    client = AsyncTestClient()
+
+    @client.listen("async_test_client.test_wait")
+    async def listen(msg):
+        global counter
+        counter += 1
+
+    @client.listen("async_test_client.test_wait_second")
+    async def listen(msg):
+        global second_counter
+        second_counter += 1
+
+    await client.start()
+    yield client
+    await client.stop()
+
+
+@pytest.mark.asyncio
+async def test_request(client: AsyncTestClient):
+    subject = "async_test_client.test_wait"
+    count_calls = 10
+    for _ in range(count_calls):
+        await client.publish(subject, {})
+    await client.wait(
+        count_calls, timeout=1
+    )  # wait for at least 10 messages to be handled
+    assert counter == count_calls
+
+
+@pytest.mark.asyncio
+async def test_request(client: AsyncTestClient):
+    subject = "async_test_client.test_wait"
+    count_calls = 15
+    for _ in range(count_calls):
+        await client.publish(subject, {})
+    await client.wait(count_calls, subject=subject)  # only for this subject
+    assert counter == count_calls
+
+
+@pytest.mark.asyncio
+async def test_request(client: AsyncTestClient):
+    subject = "async_test_client.test_wait"
+    subject_second = "async_test_client.test_wait_second"
+    count_calls = 5
+    for _ in range(count_calls):
+        await client.publish(subject, {})
+        await client.publish(subject_second, {})
+    await client.wait(
+        subjects={subject: count_calls, subject_second: count_calls}
+    )  # for more than 1 subject
+    assert counter == count_calls
+    assert second_counter == count_calls
+    assert client.count_subject_calls(subject) == count_calls
+    assert client.count_subject_calls(subject_second) == count_calls
diff --git a/tests/emulator/test_event_storage_middleware.py b/tests/emulator/test_event_storage_middleware.py
index 4fd3d78..8f64a8f 100644
--- a/tests/emulator/test_event_storage_middleware.py
+++ b/tests/emulator/test_event_storage_middleware.py
@@ -69,7 +69,7 @@ async def request(msg):
 global_object = Global()
 
 
-@pytest.fixture(scope="session")
+@pytest.fixture(scope="module")
 def client():
     client = TestClient(run_panini)
 
@@ -81,8 +81,9 @@ def publish_listener(msg):
     def listen_publish_listener(msg):
         pass
 
-    client.start()
-    return client
+    client.start(do_always_listen=False)
+    yield client
+    client.stop()
 
 
 def test_send_publish_middleware(client):
diff --git a/tests/separate_file/__init__.py b/tests/separate_file/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/separate_file/main.py b/tests/separate_file/main.py
new file mode 100644
index 0000000..03a15f4
--- /dev/null
+++ b/tests/separate_file/main.py
@@ -0,0 +1,17 @@
+from panini import app as panini_app
+
+app = panini_app.App(
+    service_name="separate_file",
+    host="127.0.0.1",
+    port=4222,
+    logger_in_separate_process=False,
+)
+
+
+@app.listen("separate_file.listen_request")
+async def listen_request(msg):
+    return {"success": True, "message": "separate_file.listen_request"}
+
+
+if __name__ == "__main__":
+    app.start()
diff --git a/tests/separate_file/test_main.py b/tests/separate_file/test_main.py
new file mode 100644
index 0000000..c74bff2
--- /dev/null
+++ b/tests/separate_file/test_main.py
@@ -0,0 +1,21 @@
+import pytest
+
+from panini.test_client import TestClient
+
+
+def run_panini():
+    from tests.separate_file.main import app
+
+    app.start()
+
+
+@pytest.fixture
+def client():
+    return TestClient(run_panini).start()
+
+
+def test_request(client):
+    subject = "separate_file.listen_request"
+    response = client.request(subject, {})
+    assert response["success"] is True
+    assert response["message"] == subject
diff --git a/tests/test_client_listen.py b/tests/test_client_listen.py
index 3397d22..43e6a6e 100755
--- a/tests/test_client_listen.py
+++ b/tests/test_client_listen.py
@@ -4,7 +4,6 @@
 # no need to run panini
 client = TestClient()
 
-
 global_object = Global()
 
 
@@ -13,6 +12,9 @@ def foo_listener(msg):
     global_object.public_variable = msg.data["data"] + 1
 
 
+client.start(do_always_listen=False)
+
+
 def test_client_listener():
     client.publish("test_client_listen.foo", {"data": 1})
     client.wait(count=1)
diff --git a/tests/test_client_listen_response.py b/tests/test_client_listen_response.py
index a5518fb..e0b3e79 100755
--- a/tests/test_client_listen_response.py
+++ b/tests/test_client_listen_response.py
@@ -13,6 +13,9 @@ def foo_listener(msg):
     return {"data": msg.data["data"] + 5}
 
 
+client.start()
+
+
 def test_client_response_listener():
     response = client.request("test_client_listen_response.foo", {"data": 1})
 
diff --git a/tests/test_diff_datatypes.py b/tests/test_diff_datatypes.py
index beb6bf8..2d3b2bd 100644
--- a/tests/test_diff_datatypes.py
+++ b/tests/test_diff_datatypes.py
@@ -106,7 +106,7 @@ def client():
     def dict_listener(msg):
         global_object.public_variable = msg.data["type"]
 
-    client.start()
+    client.start(do_always_listen=False, use_error_middleware=False)
     yield client
     client.stop()
 
diff --git a/tests/test_logs_in_separate_process.py b/tests/test_logs_in_separate_process.py
index 365d768..4892f12 100644
--- a/tests/test_logs_in_separate_process.py
+++ b/tests/test_logs_in_separate_process.py
@@ -86,4 +86,3 @@ def test_listen_composite_subject_with_response(client):
 def test_kill_logs(client):
     response = client.request("test_logs_in_separate_process.kill.logs", {})
     assert response["success"] is True
-    client.panini_process.kill()
diff --git a/tests/test_long_requests.py b/tests/test_long_requests.py
new file mode 100644
index 0000000..3a12d9a
--- /dev/null
+++ b/tests/test_long_requests.py
@@ -0,0 +1,38 @@
+import asyncio
+import time
+
+import pytest
+
+from panini.test_client import TestClient
+from panini import app as panini_app
+
+
+def run_panini():
+    app = panini_app.App(
+        service_name="test_request",
+        host="127.0.0.1",
+        port=4222,
+        logger_in_separate_process=False,
+    )
+
+    @app.listen("test_request.start")
+    async def publish_request(msg):
+        await asyncio.sleep(1.1)
+        return msg.data
+
+    app.start()
+
+
+@pytest.fixture(scope="module")
+def client():
+    client = TestClient(run_panini, socket_timeout=3, listener_socket_timeout=1)
+    client.start()
+    yield client
+    client.stop()
+
+
+def test_publish_request(client):
+    time.sleep(1.5)
+    for _ in range(3):
+        response = client.request("test_request.start", {"success": True})
+        assert response["success"] is True
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
index f6348b5..6f5415b 100644
--- a/tests/test_middleware.py
+++ b/tests/test_middleware.py
@@ -98,7 +98,7 @@ def publish_listener(msg):
     def listen_publish_listener(msg):
         global_object.another_variable = msg.data["data"] + 3
 
-    client.start()
+    client.start(do_always_listen=False)
     yield client
     client.stop()
 
diff --git a/tests/test_non_blocking_request.py b/tests/test_non_blocking_request.py
new file mode 100644
index 0000000..0f7fea8
--- /dev/null
+++ b/tests/test_non_blocking_request.py
@@ -0,0 +1,68 @@
+import asyncio
+
+import pytest
+
+from panini.test_client import TestClient
+from panini import app as panini_app
+
+msg_num = 10
+
+
+def run_panini():
+    app = panini_app.App(
+        service_name="test_non_blocking_request",
+        host="127.0.0.1",
+        port=4222,
+        logger_in_separate_process=False,
+    )
+
+    @app.listen("test_non_blocking_request.start")
+    async def non_blocking_requests(msg):
+        def handle_response(msg):
+            app.publish_sync(
+                "test_non_blocking_request.finalize",
+                {"success": True, "data_type": type(msg.data).__name__},
+            )
+
+        for _ in range(msg_num):
+            await app.request(
+                subject="test_non_blocking_request.foo",
+                message={"data": 1},
+                callback=handle_response,
+            )
+
+        return {"success": True}
+
+    @app.listen("test_non_blocking_request.foo")
+    async def subject_for_requests_listener(msg):
+        await asyncio.sleep(0.5)
+        return {"success": True, "data": "request has been processed"}
+
+    app.start()
+
+
+result = None
+
+
+@pytest.fixture(scope="module")
+def client():
+    client = TestClient(run_panini, socket_timeout=100)
+
+    @client.listen("test_non_blocking_request.finalize")
+    def helper(msg):
+        global result
+        result = msg.data
+
+    client.start(do_always_listen=False)
+    yield client
+    client.stop()
+
+
+def test_non_blocking_request(client):
+    response = client.request("test_non_blocking_request.start", {})
+    assert response["success"] is True
+
+    client.wait(msg_num)
+    assert isinstance(result, dict)
+    assert result["success"] is True
+    assert result["data_type"] == "bytes"
diff --git a/tests/test_parameter_listen_subject_only_if_exclude.py b/tests/test_parameter_listen_subject_only_if_exclude.py
new file mode 100644
index 0000000..742fa17
--- /dev/null
+++ b/tests/test_parameter_listen_subject_only_if_exclude.py
@@ -0,0 +1,46 @@
+import pytest
+
+from panini.test_client import TestClient
+from panini import app as panini_app
+
+
+def run_panini():
+    app = panini_app.App(
+        service_name="test_parameter_listen_subject_only_if_exclude",
+        host="127.0.0.1",
+        port=4222,
+        listen_subject_only_if_exclude=["start"],
+        logger_in_separate_process=False,
+    )
+
+    @app.listen("test_parameter_listen_subject_only_if_exclude.start")
+    async def start(msg):
+        return {"data": 1}
+
+    @app.listen("test_parameter_listen_subject_only_if_exclude.foo")
+    async def foo(msg):
+        return {"data": 2}
+
+    @app.listen("test_parameter_listen_subject_only_if_exclude.bar")
+    async def start(msg):
+        return {"data": 3}
+
+    app.start()
+
+
+@pytest.fixture(scope="module")
+def client():
+    client = TestClient(run_panini).start()
+    yield client
+    client.stop()
+
+
+def test_listen_subject_only_if_exclude(client):
+    response = client.request("test_parameter_listen_subject_only_if_exclude.foo", {})
+    assert response["data"] == 2
+
+    response = client.request("test_parameter_listen_subject_only_if_exclude.bar", {})
+    assert response["data"] == 3
+
+    with pytest.raises(OSError):
+        client.request("test_parameter_listen_subject_only_if_exclude.start", {})
diff --git a/tests/test_publish.py b/tests/test_publish.py
index 0de10b5..2fb0957 100755
--- a/tests/test_publish.py
+++ b/tests/test_publish.py
@@ -35,7 +35,7 @@ def bar_listener1(msg):
     def bar_listener2(msg):
         global_object.another_variable = msg.data["data"] + 1
 
-    client.start()
+    client.start(do_always_listen=False)
     yield client
     client.stop()
 
diff --git a/tests/test_reply_to.py b/tests/test_reply_to.py
index 0633f34..1c9d54f 100755
--- a/tests/test_reply_to.py
+++ b/tests/test_reply_to.py
@@ -41,7 +41,7 @@ def client():
     def bar_listener(msg):
         global_object.public_variable = msg.data["data"] + 3
 
-    client.start()
+    client.start(do_always_listen=False)
     yield client
     client.stop()
 
diff --git a/tests/test_request_listen_response.py b/tests/test_request_listen_response.py
new file mode 100644
index 0000000..176f4ed
--- /dev/null
+++ b/tests/test_request_listen_response.py
@@ -0,0 +1,41 @@
+import pytest
+from panini.test_client import TestClient
+from panini import app as panini_app
+
+
+def run_panini():
+    app = panini_app.App(
+        service_name="test_request_listen_response", host="127.0.0.1", port=4222
+    )
+
+    @app.listen("test_request_listen_response.start")
+    async def start(msg):
+        await app.request("test_request_listen_response.app.started", {"success": True})
+        return {"success": True}
+
+    app.start()
+
+
+is_app_started = False
+
+
+@pytest.fixture()
+def client():
+    client = TestClient(run_panini)
+
+    @client.listen("test_request_listen_response.app.started")
+    def app_started(msg):
+        global is_app_started
+        is_app_started = True
+        return {"started": True}
+
+    client.start()
+    yield client
+    client.stop()
+
+
+def test_get_token(client):
+    assert is_app_started is False
+    response = client.request("test_request_listen_response.start", {})
+    assert response["success"]
+    assert is_app_started is True
diff --git a/tests/test_task.py b/tests/test_task.py
index 36c69f0..40098a5 100755
--- a/tests/test_task.py
+++ b/tests/test_task.py
@@ -32,7 +32,7 @@ def client():
     def foo_listener(msg):
         global_object.public_variable = msg.data["data"] + 1
 
-    client.start()
+    client.start(do_always_listen=False)
     yield client
     client.stop()
 
diff --git a/tests/test_timer_task.py b/tests/test_timer_task.py
index c6f0466..a05e367 100755
--- a/tests/test_timer_task.py
+++ b/tests/test_timer_task.py
@@ -34,7 +34,7 @@ def client():
     def foo_listener(msg):
         global_object.another_variable += 2
 
-    client.start()
+    client.start(do_always_listen=False)
     yield client
     client.stop()
 
diff --git a/tests/test_validators.py b/tests/test_validators.py
index 3851477..d3596cf 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -20,7 +20,7 @@ class DataValidator(Validator):
 
     @app.listen("test_validator.foo", validator=DataValidator)
     async def publish(msg):
-        await app.publish(subject="test_validator.bar", message={"data": 1})
+        return {"success": True}
 
     @app.listen("test_validator.check")
     async def check(msg):
@@ -39,13 +39,7 @@ async def check(msg):
 
 @pytest.fixture(scope="module")
 def client():
-    client = TestClient(run_panini)
-
-    @client.listen("test_validator.bar")
-    def bar_listener1(msg):
-        global_object.public_variable = msg.data["data"]
-
-    client.start()
+    client = TestClient(run_panini).start()
     yield client
     client.stop()
 
@@ -62,15 +56,13 @@ def test_correct_message(client):
     assert client.request("test_validator.check", {"data": 1})["success"]
 
 
-def test_publish_correct_message(client):
-    assert global_object.public_variable == 0
-    client.publish("test_validator.foo", {"data": "string"})
-    client.wait(1)
-    assert global_object.public_variable == 1
+def test_request_with_correct_message(client):
+    response = client.request("test_validator.foo", {"data": "string"})
+    assert response["success"] is True
 
 
-def test_publish_incorrect_message(client):
-    assert global_object.public_variable == 1
-    client.publish("test_validator.foo", {"data": 1})
-    client.wait(1)
-    assert global_object.public_variable == 1
+def test_request_with_incorrect_message(client):
+    response = client.request("test_validator.foo", {"data": 1})
+    assert (
+        response["success"] is True
+    )  # both should be success = True, validator do not stop the request
diff --git a/tests/test_web_server_separately.py b/tests/test_web_server_separately.py
index b975719..e0b7483 100755
--- a/tests/test_web_server_separately.py
+++ b/tests/test_web_server_separately.py
@@ -4,8 +4,7 @@
 import pytest
 from aiohttp import web
 from panini import app as panini_app
-from panini.test_client import HTTPSessionTestClient
-from panini.utils.helper import start_process
+from panini.test_client import TestClient
 
 
 def run_panini():
@@ -43,13 +42,13 @@ async def post(self):
 # if we use raw HTTPSessionTestClient - we need to run panini manually and wait for setup
 @pytest.fixture(scope="module")
 def client():
-    client = HTTPSessionTestClient(
-        base_url="http://127.0.0.1:8083"
-    )  # handles only http requests
-    start_process(run_panini)
-    time.sleep(2)
+    client = TestClient(
+        run_panini, use_web_server=True, base_web_server_url="http://127.0.0.1:8083"
+    )
+    client.start()
+    time.sleep(1)
     yield client
-    client.close()
+    client.stop()
 
 
 @pytest.mark.parametrize(
@@ -57,13 +56,13 @@ def client():
     ["test_web_server_separately/get", "test_web_server_separately/rest/endpoint"],
 )
 def test_get(url, client):
-    response = client.get(url)
+    response = client.http_session.get(url)
     assert response.status_code == 200
     assert response.text == "get response"
 
 
 def test_get_invalid(client):
-    response = client.get("test_web_server_separately/get/invalid")
+    response = client.http_session.get("test_web_server_separately/get/invalid")
     assert response.status_code == 404, response.text
 
 
@@ -72,13 +71,13 @@ def test_get_invalid(client):
     ["test_web_server_separately/post", "test_web_server_separately/rest/endpoint"],
 )
 def test_post(url, client):
-    response = client.post(url, data=json.dumps({"data": 1}))
+    response = client.http_session.post(url, data=json.dumps({"data": 1}))
     assert response.status_code == 200
     assert response.json()["data"] == 2
 
 
 def test_post_invalid(client):
-    response = client.post(
+    response = client.http_session.post(
         "test_web_server_separately/post", data=json.dumps({"data": None})
     )
     assert response.status_code == 500, response.text