TestCase
TestCase
Sección titulada «TestCase»TestCase es la clase base que cada prueba en una aplicación Orionis debe extender. Hereda de unittest.IsolatedAsyncioTestCase, lo que significa que soporta métodos de prueba tanto síncronos como asíncronos de forma nativa. Su adición clave sobre el unittest estándar es la inyección automática del contexto de la aplicación — cada método de prueba se ejecuta dentro del contexto completo de la aplicación Orionis, con el contenedor de servicios, la configuración y todos los proveedores arrancados disponibles.
Importación
Sección titulada «Importación»from orionis.test import TestCaseEsta es la única importación necesaria para comenzar a escribir pruebas. TestCase es la única exportación pública del paquete orionis.test.
Cómo Funciona
Sección titulada «Cómo Funciona»Cuando el runner de pruebas llama a un método de prueba, TestCase intercepta el acceso al atributo a través de un hook personalizado __getattribute__. Si el nombre accedido coincide con el patrón de método configurado (por defecto test*) y es un invocable (método o función), TestCase lo envuelve en una función asíncrona que llama a Application.invoke(). Esto garantiza:
- El contenedor de servicios está activo — se pueden usar type-hints de dependencias en los métodos de prueba y estas se resuelven automáticamente, exactamente como se haría en un controlador o servicio.
- La configuración está cargada — todos los valores de
config/*.pyson accesibles a través de la instancia de la aplicación. - Los proveedores están arrancados — cada proveedor de servicio registrado ha sido
register()ado yboot()eado antes de que el código de prueba se ejecute. - El soporte async es nativo — tanto los métodos
def test...comoasync def test...funcionan. Los métodos síncronos son await-eados a través del mismo wrapper de forma transparente.
Los atributos privados (nombres que comienzan con _) y los atributos no invocables omiten el envolvimiento por completo y se retornan tal cual.
Escribir la Primera Prueba
Sección titulada «Escribir la Primera Prueba»Prueba Síncrona Básica
Sección titulada «Prueba Síncrona Básica»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)Prueba Asíncrona
Sección titulada «Prueba Asíncrona»from orionis.test import TestCase
class TestAsyncService(TestCase):
async def testFetchData(self): # Las operaciones async se ejecutan con await automáticamente 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)Dado que TestCase extiende IsolatedAsyncioTestCase, cada método de prueba async obtiene su propio bucle de eventos. No es necesario gestionar el bucle manualmente.
Pruebas con el Contenedor de Servicios
Sección titulada «Pruebas con el Contenedor de Servicios»Dado que cada método de prueba se invoca a través de Application.invoke(), el contenedor de servicios resuelve las dependencias automáticamente. Agregue un type-hint de un contrato o una clase concreta como parámetro del método y el framework inyecta la implementación registrada — exactamente como lo hace con controladores o clases de servicio:
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")Se pueden inyectar tantas dependencias como sea necesario:
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)Aserciones
Sección titulada «Aserciones»TestCase hereda la biblioteca completa de aserciones de unittest.TestCase. Cada método de aserción estándar está disponible:
Igualdad
Sección titulada «Igualdad»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) != 0Veracidad
Sección titulada «Veracidad»self.assertTrue(expr) # bool(expr) is Trueself.assertFalse(expr) # bool(expr) is FalseIdentidad y Tipo
Sección titulada «Identidad y Tipo»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)Pertenencia
Sección titulada «Pertenencia»self.assertIn(item, container) # item in containerself.assertNotIn(item, container) # item not in containerComparación
Sección titulada «Comparación»self.assertGreater(a, b) # a > bself.assertGreaterEqual(a, b) # a >= bself.assertLess(a, b) # a < bself.assertLessEqual(a, b) # a <= bExcepciones
Sección titulada «Excepciones»# Como administrador de contextowith self.assertRaises(ValueError): function_that_raises()
# Con coincidencia de mensajewith self.assertRaisesRegex(ValueError, "invalid"): function_that_raises()Coincidencia de Cadenas
Sección titulada «Coincidencia de Cadenas»self.assertRegex(text, pattern) # re.search(pattern, text)self.assertNotRegex(text, pattern) # not re.search(pattern, text)Comparación de Colecciones
Sección titulada «Comparación de Colecciones»self.assertCountEqual(a, b) # mismos elementos, sin importar el ordenself.assertSequenceEqual(a, b) # mismos elementos en el mismo ordenself.assertListEqual(a, b) # específicamente para listasself.assertDictEqual(a, b) # específicamente para diccionariosself.assertSetEqual(a, b) # específicamente para conjuntosOmitir Pruebas
Sección titulada «Omitir Pruebas»Use los decoradores estándar de unittest para omitir pruebas condicionalmente. Las pruebas omitidas reciben el estado SKIPPED y no cuentan como fallos.
Omisión Incondicional
Sección titulada «Omisión Incondicional»import unittestfrom orionis.test import TestCase
class TestFeature(TestCase):
@unittest.skip("Aún no implementado") def testPendingFeature(self): passOmisión Condicional
Sección titulada «Omisión Condicional»import sysimport unittestfrom orionis.test import TestCase
class TestPlatformSpecific(TestCase):
@unittest.skipIf(sys.platform == "win32", "No soportado en Windows") def testLinuxOnlyFeature(self): pass
@unittest.skipUnless(sys.platform.startswith("linux"), "Requiere Linux") def testLinuxBehavior(self): passOmisión Programática
Sección titulada «Omisión Programática»from orionis.test import TestCase
class TestConditional(TestCase):
def testMaybeSkip(self): if not some_precondition(): self.skipTest("Precondición no cumplida") # La lógica de la prueba continúa aquí...Setup y Teardown
Sección titulada «Setup y Teardown»TestCase soporta todos los hooks estándar de setup y teardown de unittest. Estos se ejecutan fuera del wrapper del contexto de la aplicación — solo los métodos de prueba que coincidan con el patrón de método son envueltos.
Hooks por Prueba
Sección titulada «Hooks por Prueba»from orionis.test import TestCase
class TestWithSetup(TestCase):
def setUp(self): """Se ejecuta antes de cada método de prueba.""" self.data = {"key": "value"}
def tearDown(self): """Se ejecuta después de cada método de prueba, incluso si falló.""" self.data = None
def testDataIsAvailable(self): self.assertIn("key", self.data)Hooks por Clase
Sección titulada «Hooks por Clase»from orionis.test import TestCase
class TestWithClassSetup(TestCase):
@classmethod def setUpClass(cls): """Se ejecuta una vez antes de cualquier prueba en la clase.""" cls.shared_resource = create_expensive_resource()
@classmethod def tearDownClass(cls): """Se ejecuta una vez después de todas las pruebas en la clase.""" cls.shared_resource.close()
def testUsesSharedResource(self): self.assertIsNotNone(self.shared_resource)Setup y Teardown Asíncronos
Sección titulada «Setup y Teardown Asíncronos»Dado que TestCase extiende IsolatedAsyncioTestCase, las variantes asíncronas también son soportadas:
from orionis.test import TestCase
class TestAsyncSetup(TestCase):
async def asyncSetUp(self): """Setup async — se ejecuta antes de cada prueba async.""" self.connection = await create_async_connection()
async def asyncTearDown(self): """Teardown async — se ejecuta después de cada prueba async.""" await self.connection.close()
async def testAsyncOperation(self): result = await self.connection.query("SELECT 1") self.assertIsNotNone(result)Patrón de Método
Sección titulada «Patrón de Método»Por defecto, solo los métodos cuyo nombre coincida con el patrón glob test* son reconocidos como métodos de prueba y envueltos con el contexto de la aplicación. Esto sigue la convención estándar de unittest.
Cambiar el Patrón
Sección titulada «Cambiar el Patrón»El patrón se puede cambiar a nivel de clase mediante el método de clase setMethodPattern:
from orionis.test.cases.case import TestCase
# Ahora solo los métodos que comiencen con "check" serán tratados como pruebasTestCase.setMethodPattern("check*")El patrón usa la sintaxis glob de fnmatch:
| Patrón | Coincide con |
|---|---|
test* | testCreate, testUpdate, test_delete |
test_user* | test_user_create, test_user_delete |
check* | checkValid, checkInvalid |
* | Todos los métodos públicos |
Organización de Pruebas
Sección titulada «Organización de Pruebas»Estructura de Directorios Recomendada
Sección titulada «Estructura de Directorios Recomendada»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.pyConvención de Nombres de Archivo
Sección titulada «Convención de Nombres de Archivo»El patrón de archivo predeterminado test_*.py espera que los archivos comiencen con test_. Todos los archivos en el start_dir (y sus subdirectorios) que coincidan con este patrón se cargan. Cada archivo debe contener una o más clases que extiendan TestCase.
Convención de Nombres de Método
Sección titulada «Convención de Nombres de Método»Los métodos de prueba deben comenzar con test (coincidiendo con el patrón predeterminado test*). Use nombres descriptivos en camelCase que transmitan lo que se está probando:
class TestPaymentService(TestCase):
def testChargeSucceedsWithValidCard(self): ...
def testChargeFailsWithExpiredCard(self): ...
def testRefundReturnsFullAmount(self): ...Referencia de Métodos
Sección titulada «Referencia de Métodos»| Método / Característica | Tipo | Descripción |
|---|---|---|
setMethodPattern(pattern) | classmethod | Reemplaza el patrón glob usado para identificar qué métodos son de prueba. El predeterminado es test* |
setUp() / tearDown() | instancia | Hooks estándar de setup y teardown por prueba |
setUpClass() / tearDownClass() | classmethod | Hooks que se ejecutan una vez por clase |
asyncSetUp() / asyncTearDown() | instancia | Hooks asíncronos de setup y teardown por prueba |
Todos los métodos self.assert*() | instancia | Biblioteca completa de aserciones de unittest.TestCase |
self.skipTest(reason) | instancia | Omitir programáticamente la prueba actual |
| Métodos de prueba sync y async | instancia | Tanto def test... como async def test... son soportados nativamente |
| Inyección de contexto de aplicación | automático | Cada método de prueba coincidente se envuelve para ejecutarse dentro del contexto de la aplicación Orionis |