Add smoke test for all views and context key tests
This commit is contained in:
parent
20fb21721a
commit
0666e17a25
202
expenses_manager/expenses/tests/test_smoke.py
Normal file
202
expenses_manager/expenses/tests/test_smoke.py
Normal file
@ -0,0 +1,202 @@
|
||||
"""Red de seguridad: que todas las vistas rendericen y que el contexto no pierda claves.
|
||||
|
||||
Dos agujeros que ni `manage.py check` ni el resto de la suite cubren:
|
||||
|
||||
1. Un `{% url %}` mal escrito o una plantilla que falta solo revienta al pedir
|
||||
la página. Es lo que pasó con `settings/index.html`, que estuvo roto sin que
|
||||
nada avisara.
|
||||
2. Un refactor que se lleva una clave del contexto por delante. Ya pasó dos
|
||||
veces: `by_month` renombrado a `chart_data`, y `last_expenses` sustituido por
|
||||
`movements`. Las claves que usan muchos tests están cubiertas de rebote; las
|
||||
que se usan en un solo sitio se pierden en silencio.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
from expenses import urls as expenses_urls
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 1. Smoke test de renderizado
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
# Rutas que a propósito NO devuelven 200. Van una por línea con su motivo: la
|
||||
# alternativa (aceptar cualquier 2xx/3xx) haría pasar el test aunque la vista
|
||||
# estuviera rota, que es justo lo contrario de lo que se busca aquí.
|
||||
NOT_RENDERABLE = {
|
||||
# Endpoint de descarga, no página: valida ExportForm(request.GET) y sin
|
||||
# `kind` ni `fmt` responde 400 por diseño. Su cobertura está en test_export.
|
||||
"export_download",
|
||||
}
|
||||
|
||||
|
||||
def _argument_free_route_names():
|
||||
"""Rutas del URLconf que se pueden pedir sin argumentos.
|
||||
|
||||
Se saca del URLconf y no de una lista escrita a mano: así una vista nueva
|
||||
entra en el smoke test el día que se añade, sin que nadie se acuerde. Una
|
||||
lista a mano se queda desfasada y el test deja de servir en silencio.
|
||||
"""
|
||||
return [
|
||||
pattern.name
|
||||
for pattern in expenses_urls.urlpatterns
|
||||
if pattern.pattern.regex.groups == 0
|
||||
and pattern.name
|
||||
and pattern.name not in NOT_RENDERABLE
|
||||
]
|
||||
|
||||
|
||||
ROUTE_NAMES = _argument_free_route_names()
|
||||
|
||||
|
||||
def test_the_route_list_is_not_empty():
|
||||
# Si el recorrido del URLconf se rompiera, el parametrize saldría vacío y
|
||||
# los tests de abajo "pasarían" sin pedir una sola página.
|
||||
assert len(ROUTE_NAMES) > 15
|
||||
|
||||
|
||||
@pytest.mark.parametrize("route_name", ROUTE_NAMES)
|
||||
def test_every_view_renders(auth_client, route_name):
|
||||
# Una ruta por caso para que el fallo diga qué vista se rompió.
|
||||
response = auth_client.get(reverse(route_name))
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.parametrize("route_name", sorted(NOT_RENDERABLE))
|
||||
def test_the_excluded_routes_are_still_excluded_for_a_reason(auth_client, route_name):
|
||||
# Si una excluida empieza a devolver 200, la exclusión sobra y hay que
|
||||
# quitarla: así la lista no se queda con motivos caducados dentro.
|
||||
response = auth_client.get(reverse(route_name))
|
||||
|
||||
assert response.status_code != 200
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 2. Claves del contexto
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
DASHBOARD_CONTEXT_KEYS = {
|
||||
# Navegación y fecha de referencia
|
||||
"active_menu",
|
||||
"today",
|
||||
# KPIs
|
||||
"kpi_total",
|
||||
"kpi_count",
|
||||
"kpi_categories",
|
||||
"kpi_balance",
|
||||
# Comparativa con el periodo anterior
|
||||
"compare_enabled",
|
||||
"compare_suppressed",
|
||||
"kpi_previous_total",
|
||||
"kpi_difference",
|
||||
"kpi_difference_abs",
|
||||
"kpi_percentage",
|
||||
"kpi_trend",
|
||||
"category_comparison",
|
||||
# Gráficos: eje compartido, serie de gastos, serie de ingresos y desglose
|
||||
"chart_labels",
|
||||
"chart_data",
|
||||
"chart_income_data",
|
||||
"chart_type",
|
||||
"by_category",
|
||||
"by_category_chart",
|
||||
"accounts_charts",
|
||||
# Filtros de periodo, cuenta y rango libre
|
||||
"period",
|
||||
"year_list",
|
||||
"months",
|
||||
"selected_year",
|
||||
"selected_month",
|
||||
"date_from",
|
||||
"date_to",
|
||||
"range_active",
|
||||
"accounts",
|
||||
"selected_account",
|
||||
"selected_account_obj",
|
||||
# Objetivos y listado reciente
|
||||
"goals",
|
||||
"recent_expenses",
|
||||
# Medias y proyección
|
||||
"daily_average",
|
||||
"projected_end_of_month",
|
||||
}
|
||||
|
||||
HOME_CONTEXT_KEYS = {
|
||||
"active_menu",
|
||||
# KPIs del mes y su comparación con el anterior
|
||||
"kpi_total",
|
||||
"kpi_count",
|
||||
"kpi_categories",
|
||||
"prev_total",
|
||||
"diff_amount",
|
||||
"diff_pct",
|
||||
# Saldos y alertas
|
||||
"total_balance",
|
||||
"account_balances",
|
||||
"negative_accounts",
|
||||
"has_alerts",
|
||||
# Objetivos
|
||||
"goals",
|
||||
"exceeded_goals",
|
||||
# Movimientos recientes y la mini gráfica
|
||||
"movements",
|
||||
"mini_chart_labels",
|
||||
"mini_chart_data",
|
||||
}
|
||||
|
||||
|
||||
def _assert_context_has(response, expected_keys):
|
||||
"""Inclusión, nunca igualdad.
|
||||
|
||||
Lo que hay que detectar es que falte una clave. Que aparezca una nueva es lo
|
||||
normal al añadir una funcionalidad, así que endurecer esto a igualdad exacta
|
||||
daría un test que falla cada vez que alguien amplía la vista, y acabaría
|
||||
borrado por pesado. No lo cambies a ==.
|
||||
"""
|
||||
missing = sorted(key for key in expected_keys if key not in response.context)
|
||||
|
||||
assert not missing, "faltan claves en el contexto: %s" % ", ".join(missing)
|
||||
|
||||
|
||||
def test_dashboard_context_keeps_all_its_keys(auth_client):
|
||||
response = _dashboard(auth_client)
|
||||
|
||||
_assert_context_has(response, DASHBOARD_CONTEXT_KEYS)
|
||||
|
||||
|
||||
def test_dashboard_context_keeps_its_keys_with_comparison_on(auth_client):
|
||||
# La comparativa rellena claves que en el camino normal se quedan a cero.
|
||||
response = _dashboard(auth_client, compare="1")
|
||||
|
||||
_assert_context_has(response, DASHBOARD_CONTEXT_KEYS)
|
||||
|
||||
|
||||
def test_dashboard_context_keeps_its_keys_with_a_free_range(auth_client):
|
||||
# El rango libre entra por otra rama del eje, y es la que trae
|
||||
# compara_suppressed y deja projected_end_of_month a cero.
|
||||
response = _dashboard(auth_client, date_from="2024-03-01", date_to="2024-03-30")
|
||||
|
||||
_assert_context_has(response, DASHBOARD_CONTEXT_KEYS)
|
||||
|
||||
|
||||
def test_dashboard_context_keeps_its_keys_with_a_preset(auth_client):
|
||||
response = _dashboard(auth_client, period="this_month")
|
||||
|
||||
_assert_context_has(response, DASHBOARD_CONTEXT_KEYS)
|
||||
|
||||
|
||||
def test_home_context_keeps_all_its_keys(auth_client):
|
||||
response = auth_client.get(reverse("home"))
|
||||
|
||||
_assert_context_has(response, HOME_CONTEXT_KEYS)
|
||||
|
||||
|
||||
def _dashboard(client, **params):
|
||||
response = client.get(reverse("dashboard"), params)
|
||||
|
||||
assert response.status_code == 200
|
||||
return response
|
||||
Loading…
Reference in New Issue
Block a user