Skip to content

Commit 53cb7bc

Browse files
authored
Add support for async test coroutines (#292)
This commit adds `AsyncTestCase` and `AsyncViewTestCase` for testing asyncio coroutines, which interact with Sublime Text. Both classes work pretty much like `DeferrableTestCase`, except they can `await` coroutines. Initial implementation inherits `AsyncTestCase` from `DeferrableTestCase` to explicitly provide a new API with async coroutine support. Each coroutine function is scheduled for execution in `sublime_aio`'s default event loop. Underlying test method awaits completion via `yield future.done` to enable coroutines to run synchronous tasks in main thread via `sublime.set_timeout()`. Yielding from async coroutines is not supported.
1 parent ab20cf2 commit 53cb7bc

14 files changed

Lines changed: 340 additions & 121 deletions

File tree

README.md

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -562,30 +562,96 @@ see also [tests/test_defer.py](https://github.com/randy3k/UnitTesting-example/bl
562562

563563
### Asyncio testing
564564

565-
Tests for `asyncio` are written using `IsolatedAsyncioTestCase` class.
565+
Tests for `asyncio` use `AsyncTestCase` or `AsyncViewTestCase` class.
566+
567+
It auto-detects type of `setUp()`, `tearDown()`, and `test_..()` methods.
568+
Those can be synchronous methods or async coroutine functions.
569+
570+
Asynchronous coroutine functions are executed in default event loop,
571+
provided by [sublime_aio][].
566572

567573

568574
```py
569575
import asyncio
576+
import sublime
577+
578+
from unittesting import AsyncTestCase
579+
580+
581+
async def async_coroutine(view):
582+
583+
def run_in_mainthread():
584+
view.run_command("select_all")
585+
view.run_command("right_delete")
586+
view.run_command("insert", {"characters": "Modified Content"})
587+
588+
sublime.set_timeout(run_in_mainthread, 10)
589+
await asyncio.sleep(2.0)
590+
591+
592+
class MyAsyncTestCase(AsyncTestCase):
593+
594+
@classmethod
595+
async def setUpClass(cls):
596+
pass
597+
598+
@classmethod
599+
async def tearDownClass(cls):
600+
pass
570601

571-
from unittesting import IsolatedAsyncioTestCase
602+
async def setUp(self):
603+
self.view = sublime.active_window().new_file()
604+
self.view.set_scratch(True)
605+
self.view.run_command("insert", {"characters": "Initial Content"})
572606

573-
async def a_coro():
574-
return 1 + 1
607+
async def tearDown(self):
608+
self.view.close()
575609

576-
class MyAsyncTestCase(IsolatedAsyncioTestCase):
577-
async def test_something(self):
578-
result = await a_coro()
579-
await asyncio.sleep(1)
580-
self.assertEqual(result, 2)
610+
async def test_setup_completed(self):
611+
self.assertEqual(
612+
self.view.substr(sublime.Region(0, self.view.size())),
613+
"Initial Content"
614+
)
615+
616+
async def test_coroutine(self):
617+
await async_coroutine(self.view)
618+
self.assertEqual(
619+
self.view.substr(sublime.Region(0, self.view.size())),
620+
"Modified Content"
621+
)
622+
```
623+
624+
To run coroutines in a custom event loop, override static `run_override()` method.
625+
626+
```py
627+
import sublime_aio
628+
from unittesting import AsyncTestCase
629+
630+
631+
class MyAsyncTestCase(AsyncTestCase):
632+
633+
def run_coroutine(coro: abc.meta.Coroutine) -> cuncurrent.futures.Future:
634+
return sublime_aio.run_coroutine(coro)
581635
```
582636

637+
Note, asyncio event loops must not block Sublime Text's main thread.
638+
639+
> [!WARNING]
640+
>
641+
> Do not use `unittest.IsolatedAsyncioTestCase` class,
642+
> as it spins up a blocking event loop in main thread,
643+
> which prevents any synchronous command from being executed
644+
> by Sublime Text.
645+
646+
[sublime_aio]: https://github.com/packagecontrol/sublime_aio
647+
583648

584649
## Helper TestCases
585650

586651
UnitTesting provides some helper test case classes,
587652
which perform common tasks such as overriding preferences, setting up views, etc.
588653

654+
- AsyncViewTestCase
589655
- DeferrableViewTestCase
590656
- OverridePreferencesTestCase
591657
- TempDirectoryTestCase

dependencies.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
{
22
"*": {
3-
">3000": [
3+
"3000 - 3999": [
44
"coverage"
5+
],
6+
">4000": [
7+
"coverage",
8+
"sublime_aio"
59
]
610
}
711
}
Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,44 @@
11
import asyncio
22

3-
from unittest import skipIf
4-
from unittesting import IsolatedAsyncioTestCase
3+
from unittesting import AsyncViewTestCase
54

65

7-
async def a_coro():
8-
return 1 + 1
6+
async def a_coro(test):
7+
await asyncio.sleep(1.0)
8+
test.setText("Modified Content")
99

1010

11-
class MyAsyncTestCase(IsolatedAsyncioTestCase):
12-
async def test_something(self):
13-
result = await a_coro()
14-
await asyncio.sleep(1)
15-
self.assertEqual(result, 2)
11+
class MyAsyncTestCaseA(AsyncViewTestCase):
12+
13+
test_class_initiated = 0
14+
15+
@classmethod
16+
async def setUpClass(cls):
17+
assert cls.test_class_initiated == 0
18+
cls.test_class_initiated = 1
19+
20+
@classmethod
21+
async def tearDownClass(cls):
22+
cls.test_class_initiated = 2
23+
24+
async def setUp(self):
25+
self.setText("Initial Content")
26+
27+
async def tearDown(self):
28+
self.setText("")
29+
30+
async def test_class_setup_completed(self):
31+
self.assertEqual(self.test_class_initiated, 1)
32+
33+
async def test_setup_completed(self):
34+
self.assertViewContentsEqual("Initial Content")
35+
36+
async def test_coroutine(self):
37+
await a_coro(self)
38+
self.assertViewContentsEqual("Modified Content")
39+
40+
41+
class MyAsyncTestCaseB(AsyncViewTestCase):
42+
43+
async def test_class_setup_completed(self):
44+
self.assertEqual(MyAsyncTestCaseA.test_class_initiated, 2)

tests/_Asyncio/unittesting.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"deferred": false
2+
"deferred": true
33
}

tests/_Deferred/tests/test.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1+
from unittesting import TestCase
12
from unittesting import DeferrableViewTestCase
23
from unittesting import expectedFailure
34

45

6+
class TestDefaultTestCase(TestCase):
7+
8+
def test_simple_assert(self):
9+
self.assertTrue(True)
10+
11+
512
class TestDeferrable(DeferrableViewTestCase):
613

14+
def test_simple_assert(self):
15+
self.assertTrue(True)
16+
717
def test_defer(self):
818
self.setText("foo")
919
self.setCaretTo(0, 0)

unittesting/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from .core import AWAIT_WORKER
2+
from .core import AsyncTestCase
23
from .core import DeferrableMethod
34
from .core import DeferrableTestCase
4-
from .core import IsolatedAsyncioTestCase
55
from .core import TestCase
66
from .core import expectedFailure
7+
from .helpers import AsyncViewTestCase
78
from .helpers import DeferrableViewTestCase
89
from .helpers import OverridePreferencesTestCase
910
from .helpers import TempDirectoryTestCase
@@ -12,12 +13,13 @@
1213

1314

1415
__all__ = [
16+
"AsyncTestCase",
17+
"AsyncViewTestCase",
1518
"AWAIT_WORKER",
1619
"DeferrableMethod",
1720
"DeferrableTestCase",
1821
"DeferrableViewTestCase",
1922
"expectedFailure",
20-
"IsolatedAsyncioTestCase",
2123
"OverridePreferencesTestCase",
2224
"run_scheduler",
2325
"TempDirectoryTestCase",

unittesting/core/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
11
import sys
22

33
if sys.version_info >= (3, 13):
4+
from .py313.case import AsyncTestCase
45
from .py313.case import DeferrableMethod
56
from .py313.case import DeferrableTestCase
6-
from .py313.case import IsolatedAsyncioTestCase
77
from .py313.case import TestCase
88
from .py313.case import expectedFailure
99
from .py313.loader import DeferrableTestLoader
1010
from .py313.runner import AWAIT_WORKER
1111
from .py313.runner import DeferringTextTestRunner
1212
from .py313.suite import DeferrableTestSuite
1313
elif sys.version_info >= (3, 8):
14+
from .py38.case import AsyncTestCase
1415
from .py38.case import DeferrableMethod
1516
from .py38.case import DeferrableTestCase
16-
from .py38.case import IsolatedAsyncioTestCase
1717
from .py38.case import TestCase
1818
from .py38.case import expectedFailure
1919
from .py38.loader import DeferrableTestLoader
2020
from .py38.runner import AWAIT_WORKER
2121
from .py38.runner import DeferringTextTestRunner
2222
from .py38.suite import DeferrableTestSuite
2323
elif sys.version_info >= (3, 3):
24+
from .py33.case import AsyncTestCase
2425
from .py33.case import DeferrableMethod
2526
from .py33.case import DeferrableTestCase
26-
from .py33.case import IsolatedAsyncioTestCase
2727
from .py33.case import TestCase
2828
from .py33.case import expectedFailure
2929
from .py33.loader import DeferrableTestLoader
@@ -34,13 +34,13 @@
3434
raise ImportError("Unsupported python runtime!")
3535

3636
__all__ = [
37+
"AsyncTestCase",
3738
"AWAIT_WORKER",
3839
"DeferrableMethod",
3940
"DeferrableTestCase",
4041
"DeferrableTestLoader",
4142
"DeferrableTestSuite",
4243
"DeferringTextTestRunner",
43-
"IsolatedAsyncioTestCase",
4444
"TestCase",
4545
"expectedFailure",
4646
]

0 commit comments

Comments
 (0)