Saltar al contenido principal

wiki-_-Test-Automaticos

Ver en Git


_ Test Automaticos

Test automaticos

Antes de leer esta guia, leer la guia de desarrollo https://git.auravant.com/it/backend/aurapi-v2/-/wikis/_-Guia-de-desarrollo

|-- tests/
| |-- e2e
| | |-- shared/
| | |-- feature_1/
| | |-- feature_2/
| |-- units
| | |-- shared/
| | |-- feature_1/
| | |-- feature_2/

Libreria testing

pytest==8.3.4
pytest-asyncio==0.24.0

Units

Estos test tienen como objetivo testear la logica de negocio. Como minino debemos testear los casos de uso. Estos Para estos test debemos mockear o crear una clase fake de las interfaces que se tienen que implementar en la capa de infraestructura. Si hay una clase o funcion dentro de dominio que contiene logica critica o compleja si se considera necesario tambien agregar unit test

Ejemplo

Caso de uso a testear

class LoginFieldview:
def __init__(
self,
login_fieldview_repository: LoginFieldviewRepositoryABC,
login_fieldview_api_client: LoginFieldviewApiClientABC,
):
self._login_fieldview_repository = login_fieldview_repository
self._login_fieldview_api_client = login_fieldview_api_client

async def execute(self, user_id: int, code: str):
"""genera access_token y refresh_token y lo guarda en db

Args:
user_id (int):
code (str): codigo de fieldview para generar token, este codigo es valido aproximadamente por 1 minuto
"""
url_redirect = self._login_fieldview_api_client.get_url_redirect(user_id)
login_user_fieldview = await self._login_fieldview_api_client.generate_token(user_id, url_redirect, code)

await self._login_fieldview_repository.save(login_user_fieldview)

Lo primero que tengo que hacer es crear una implementacion de LoginFieldviewRepositoryABC, LoginFieldviewApiClientABC para mis test (esto podria reemplazarse por mocks). Se podria combinar tanto clases Fakes como mocks, Para repositorios o clase que tengan que mantener estados una clase fake me parece mas facil. Los mocks los usuarios para clase que no mantengan estados y me interese comprobar si se ejecutaron y con que parametros. El el siguiente ejemplos FakeLoginFieldviewApiClient se podria reemplazar facil por un mock. Y verificar que se llame a la funcion get_url_redirect con los parametros correctos.

# tests/units/fieldview/fakes/login_repository_fake.py
class FakeLoginFieldviewRepository(LoginFieldviewRepositoryABC):
def __init__(self, login_users: dict[int, LoginUserFieldview] = {}):
self.login_users: dict[int, LoginUserFieldview] = login_users

async def save(self, login_user: LoginUserFieldview):
self.login_users[login_user.user_id] = login_user

async def get(self, user_id: int) -> LoginUserFieldview | None:
return self.login_users.get(user_id)

async def delete(self, user_id: int):
if user_id in self.login_users:
del self.login_users[user_id]

# tests/units/fieldview/fakes/login_api_client_fake.py
class FakeLoginFieldviewApiClient(LoginFieldviewApiClientABC):
async def generate_token(self, user_id: int, url_redirect: str, code: str) -> LoginUserFieldview:
access_token = f"test_access_token_{user_id}"
refresh_token = f"test_refresh_token_{user_id}"
return LoginUserFieldview(user_id, access_token, refresh_token)

async def refresh_token(self, user_id: int, refresh_token: str) -> LoginUserFieldview | None:
access_token = f"test_access_token_{user_id}"
refresh_token = f"test_refresh_token_{user_id}"
return LoginUserFieldview(user_id, access_token, refresh_token)

def get_url_redirect(self, user_id: int) -> str:
# Replace with your test redirect URL.
return f"test_redirect_url_{user_id}"

def get_url_login(self, user_id: int) -> str:
# Replace with your test login URL.
return f"test_login_url_{user_id}"

De esta manera el test me queda simple

async def test_login_fielview_ok():
# Preparacion de datos
user_id = 10
code = "test_code"
fake_repo = FakeLoginFieldviewRepository()
fake_client = FakeLoginFieldviewApiClient()
use_case = LoginFieldview(fake_repo, fake_client)

# Ejecucion del test
await use_case.execute(user_id, code)

# Verificacion de resultados
assert user_id in fake_repo.login_users
assert fake_repo.login_users[user_id].access_token == f"test_access_token_{user_id}"
assert fake_repo.login_users[user_id].refresh_token == f"test_refresh_token_{user_id}"

Test e2e

Estos test deben testear desde el punto de entrada a una aplicacion, solo mockear consultas a apis externas. La base de datos debe ser solo para los test(esto se puede levantar con docker-compose.test.yml)

conftest.py

Este archivo se ejecutara en cada test que dentro de su carpeta, esto incluye las subcarpetas. Aca puedo definir fixture a usar en los test

  • test_client: Solo se ejecuta si el test hace use de el
  • setup: esta como autouse por lo tanto se ejecutara en todos los tests que esten dentro de la carpeta e2e. Con esta configuracion de setup tenemos una conexion a la base de datos de pruebas y la validacion de usuario. Tener en cuenta aca que todos las apis que validen login tendran el usuario : tests/e2e/shared/login_mock.py::user_request_test. Si se necesita agregar alguna configuracion o algun fixture comun para toda la aplicacion agregarlo aca. Si es algo especifico del modulo a testear crear un conftest.py en el modulo https://docs.pytest.org/en/stable/how-to/fixtures.html
# tests/e2e/conftest.py

@pytest_asyncio.fixture
async def test_client():
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client_api:
yield client_api


@pytest_asyncio.fixture(autouse=True)
async def setup():
# db_connection Se debera implementar en cada modulo para crear y borrar las tablas que necesite
AppContainer.infra_container.db_connection.override(providers.Object(get_db_test()))

# Simula login en todas las apis
app.dependency_overrides[require_login] = require_login_test
app.dependency_overrides[require_login_apps_only] = require_login_test
app.dependency_overrides[require_login_open_api] = require_login_test

Todos los modulos deberian tener un fixture que cree las tablas de la base de datos a usar. Importante que luego de ejecutar el test borre las tablas asi tenemos controlados los datos en cada test

# tests/e2e/fieldview/conftest.py
@pytest_asyncio.fixture(autouse=True)
async def setup_db():
"""Crea y elimina la base de datos por cada tests."""
db = get_db_test()
for sql in create_tables_sql_fieldview:
await db.execute(sql)

try:
yield
finally:
for sql in drop_tables_sql_fieldview:
await db.execute(sql)
await db.close()

Ejemplo

Tomenos como ejemplo la api del caso de uso que testeamos en units test

@login_field_view_router.get("/login/{user_id}")
async def login_fieldview(
user_id: int,
code: str,
login_use_case: LoginFieldview = Depends(lambda: AppContainer.fieldview_container.login_fieldview_use_case()),
):
await login_use_case.execute(user_id, code)

Para saber si tengo que mockear algo debo entrar bien en detalle. La api usa el caso de uso LoginFieldview este usa:

  • LoginFieldviewRepository: Me interesa testearlo
  • LoginFieldviewApiClient: No quiero testear porque sino mis test dependen de fieldview. Por lo tanto lo mockeo. Esto lo logro reemplazando la dependencia en mis containers(importante despues del test resetear la dependencia para que no tenga efecto en los demas)
@pytest.mark.asyncio
async def test_login_user_api(test_client):
# Preparacion
# Mock de client fieldview mas reemplazo de dependencia
mock_login_field_client = unittest.mock.AsyncMock(spec=LoginFieldviewApiClientABC)
mock_login_data = LoginUserFieldview(user_request_test.user_id, "mock_access_token", "mock_refresh_token")
mock_login_field_client.generate_token.return_value = mock_login_data
AppContainer.fieldview_container.login_fieldview_api_client.override(providers.Object(mock_login_field_client))

# Ejecucion del test
response = await test_client.get(f'fieldview/auth/login/{user_request_test.user_id}?code=codigo')

# Verificacion de resultados
mock_login_field_client.generate_token.assert_awaited_once_with(
user_request_test.user_id, unittest.mock.ANY, 'codigo'
)
assert response.status_code == 200
AppContainer.fieldview_container.login_fieldview_api_client.reset_override()

Que falta?

Realizando varios test a los casos de uso y pocos de e2e, estamos bastante cubierto. Que se podria agregar:

  • Clases o funciones de dominio, Solo si se considera necesario ya que los test hay que mantenerlos y los test muy especificos cambian con facilidad
  • Units a Repositorios: En los test e2e estamos probando el repositorio, si se considera necesario se podria agregar units test a estos, si el repositorio tiene querys complejas o devuelve un objeto complejo puede tener valor estos test
  • Test a las clase api_clients: Estos no se como realizarlos, Se podria hacer mock a las clases de request para mockear la respuesta de los servicios de terceros