TestCase
TestCase
Section titled โTestCaseโTestCase is the base class that every test in an Orionis application must extend. It inherits from unittest.IsolatedAsyncioTestCase, which means it supports both synchronous and asynchronous test methods out of the box. Its key addition over standard unittest is automatic application-context injection โ every test method runs inside the full Orionis application context, with the service container, configuration, and all bootstrapped providers available.
from orionis.test import TestCaseThis is the only import needed to start writing tests. TestCase is the sole public export of the orionis.test package.
How It Works
Section titled โHow It WorksโWhen the test runner calls a test method, TestCase intercepts the attribute access through a custom __getattribute__ hook. If the accessed name matches the configured method pattern (default test*) and is a callable (method or function), TestCase wraps it in an asynchronous function that calls Application.invoke(). This ensures:
- Service container is live โ you can type-hint dependencies on your test methods and have them resolved automatically, exactly as you would in a controller or service.
- Configuration is loaded โ all values from
config/*.pyare accessible via the application instance. - Providers are bootstrapped โ every registered service provider has been
register()ed andboot()ed before your test code runs. - Async support is native โ both
def test...andasync def test...methods work. Synchronous methods are awaited through the same wrapper transparently.
Private attributes (names starting with _) and non-callable attributes bypass the wrapping entirely and are returned as-is.
Writing Your First Test
Section titled โWriting Your First TestโBasic Synchronous Test
Section titled โBasic Synchronous Testโfrom orionis.test import TestCase
class TestMathOperations(TestCase):
def testAddition(self): self.assertEqual(1 + 1, 2)
def testSubtraction(self): result = 10 - 3 self.assertGreater(result, 0) self.assertEqual(result, 7)Asynchronous Test
Section titled โAsynchronous Testโfrom orionis.test import TestCase
class TestAsyncService(TestCase):
async def testFetchData(self): # Async operations are awaited automatically data = await some_async_service.fetch() self.assertIsNotNone(data) self.assertIn("key", data)
async def testAsyncExceptionHandling(self): with self.assertRaises(ValueError): await some_async_service.validate(invalid_input)Since TestCase extends IsolatedAsyncioTestCase, each async test method gets its own event loop. There is no need to manage the loop manually.
Testing with the Service Container
Section titled โTesting with the Service ContainerโBecause every test method is invoked through Application.invoke(), the service container resolves dependencies automatically. Type-hint a contract or a concrete class as a method parameter and the framework injects the registered implementation โ exactly as it does for controllers or service classes:
from orionis.test import TestCasefrom app.contracts.user_service import IUserService
class TestUserService(TestCase):
async def testUserCreation(self, user_service: IUserService): user = await user_service.create(name="John", email="john@example.com") self.assertIsNotNone(user.id) self.assertEqual(user.name, "John")You can inject as many dependencies as needed:
from orionis.test import TestCasefrom app.contracts.user_service import IUserServicefrom app.contracts.notification_service import INotificationService
class TestNotifications(TestCase):
async def testWelcomeEmailIsSent( self, user_service: IUserService, notifications: INotificationService, ): user = await user_service.create(name="Jane", email="jane@example.com") result = await notifications.sendWelcome(user) self.assertTrue(result)Assertions
Section titled โAssertionsโTestCase inherits the complete assertion library from unittest.TestCase. Every standard assertion method is available:
Equality
Section titled โEqualityโself.assertEqual(a, b) # a == bself.assertNotEqual(a, b) # a != bself.assertAlmostEqual(a, b) # round(a - b, 7) == 0self.assertNotAlmostEqual(a, b) # round(a - b, 7) != 0Truthiness
Section titled โTruthinessโself.assertTrue(expr) # bool(expr) is Trueself.assertFalse(expr) # bool(expr) is FalseIdentity and Type
Section titled โIdentity and Typeโself.assertIs(a, b) # a is bself.assertIsNot(a, b) # a is not bself.assertIsNone(value) # value is Noneself.assertIsNotNone(value) # value is not Noneself.assertIsInstance(obj, cls) # isinstance(obj, cls)self.assertNotIsInstance(obj, cls) # not isinstance(obj, cls)Membership
Section titled โMembershipโself.assertIn(item, container) # item in containerself.assertNotIn(item, container) # item not in containerComparison
Section titled โComparisonโself.assertGreater(a, b) # a > bself.assertGreaterEqual(a, b) # a >= bself.assertLess(a, b) # a < bself.assertLessEqual(a, b) # a <= bExceptions
Section titled โExceptionsโ# As a context managerwith self.assertRaises(ValueError): function_that_raises()
# With message matchingwith self.assertRaisesRegex(ValueError, "invalid"): function_that_raises()String Matching
Section titled โString Matchingโself.assertRegex(text, pattern) # re.search(pattern, text)self.assertNotRegex(text, pattern) # not re.search(pattern, text)Collection Comparison
Section titled โCollection Comparisonโself.assertCountEqual(a, b) # same elements, regardless of orderself.assertSequenceEqual(a, b) # same elements in the same orderself.assertListEqual(a, b) # specifically for listsself.assertDictEqual(a, b) # specifically for dictsself.assertSetEqual(a, b) # specifically for setsSkipping Tests
Section titled โSkipping TestsโUse the standard unittest decorators to conditionally skip tests. Skipped tests receive the SKIPPED status and do not count as failures.
Unconditional Skip
Section titled โUnconditional Skipโimport unittestfrom orionis.test import TestCase
class TestFeature(TestCase):
@unittest.skip("Not implemented yet") def testPendingFeature(self): passConditional Skip
Section titled โConditional Skipโimport sysimport unittestfrom orionis.test import TestCase
class TestPlatformSpecific(TestCase):
@unittest.skipIf(sys.platform == "win32", "Not supported on Windows") def testLinuxOnlyFeature(self): pass
@unittest.skipUnless(sys.platform.startswith("linux"), "Linux required") def testLinuxBehavior(self): passProgrammatic Skip
Section titled โProgrammatic Skipโfrom orionis.test import TestCase
class TestConditional(TestCase):
def testMaybeSkip(self): if not some_precondition(): self.skipTest("Precondition not met") # Test logic continues here...Setup and Teardown
Section titled โSetup and TeardownโTestCase supports all standard unittest setup and teardown hooks. These run outside the application context wrapper โ only test methods matching the method pattern are wrapped.
Per-Test Hooks
Section titled โPer-Test Hooksโfrom orionis.test import TestCase
class TestWithSetup(TestCase):
def setUp(self): """Runs before each test method.""" self.data = {"key": "value"}
def tearDown(self): """Runs after each test method, even if it failed.""" self.data = None
def testDataIsAvailable(self): self.assertIn("key", self.data)Per-Class Hooks
Section titled โPer-Class Hooksโfrom orionis.test import TestCase
class TestWithClassSetup(TestCase):
@classmethod def setUpClass(cls): """Runs once before any test in the class.""" cls.shared_resource = create_expensive_resource()
@classmethod def tearDownClass(cls): """Runs once after all tests in the class.""" cls.shared_resource.close()
def testUsesSharedResource(self): self.assertIsNotNone(self.shared_resource)Async Setup and Teardown
Section titled โAsync Setup and TeardownโSince TestCase extends IsolatedAsyncioTestCase, async variants are also supported:
from orionis.test import TestCase
class TestAsyncSetup(TestCase):
async def asyncSetUp(self): """Async setup โ runs before each async test.""" self.connection = await create_async_connection()
async def asyncTearDown(self): """Async teardown โ runs after each async test.""" await self.connection.close()
async def testAsyncOperation(self): result = await self.connection.query("SELECT 1") self.assertIsNotNone(result)Method Pattern
Section titled โMethod PatternโBy default, only methods whose name matches the glob pattern test* are recognized as test methods and wrapped with the application context. This follows the standard unittest convention.
Changing the Pattern
Section titled โChanging the PatternโThe pattern can be changed at the class level via the setMethodPattern class method:
from orionis.test.cases.case import TestCase
# Now only methods starting with "check" will be treated as testsTestCase.setMethodPattern("check*")The pattern uses fnmatch glob syntax:
| Pattern | Matches |
|---|---|
test* | testCreate, testUpdate, test_delete |
test_user* | test_user_create, test_user_delete |
check* | checkValid, checkInvalid |
* | Every public method |
Test Organization
Section titled โTest OrganizationโRecommended Directory Structure
Section titled โRecommended Directory Structureโtests/โโโ __init__.pyโโโ unit/โ โโโ __init__.pyโ โโโ test_user_service.pyโ โโโ test_order_service.pyโโโ integration/โ โโโ __init__.pyโ โโโ test_database.pyโ โโโ test_api.pyโโโ feature/ โโโ __init__.py โโโ test_checkout_flow.pyFile Naming Convention
Section titled โFile Naming ConventionโThe default file pattern test_*.py expects files to start with test_. All files in the start_dir (and its subdirectories) matching this pattern are loaded. Each file should contain one or more classes extending TestCase.
Method Naming Convention
Section titled โMethod Naming ConventionโTest methods should start with test (matching the default test* pattern). Use descriptive camelCase names that convey what is being tested:
class TestPaymentService(TestCase):
def testChargeSucceedsWithValidCard(self): ...
def testChargeFailsWithExpiredCard(self): ...
def testRefundReturnsFullAmount(self): ...Method Reference
Section titled โMethod Referenceโ| Method / Feature | Type | Description |
|---|---|---|
setMethodPattern(pattern) | classmethod | Replaces the glob pattern used to identify which methods are test methods. Default is test* |
setUp() / tearDown() | instance | Standard per-test setup and teardown hooks |
setUpClass() / tearDownClass() | classmethod | Hooks that run once per class |
asyncSetUp() / asyncTearDown() | instance | Async per-test setup and teardown hooks |
All self.assert*() methods | instance | Full unittest.TestCase assertion library |
self.skipTest(reason) | instance | Programmatically skip the current test |
| Sync and async test methods | instance | Both def test... and async def test... are supported natively |
| Application context injection | automatic | Every matched test method is wrapped to run inside the Orionis application context |