Saltar al contenido
🐍 Curso de Python
/

Módulo 5 · Lección 22 de 30

Versionado y pruebas

Cómo cambiar una API sin romper a quien la usa, y las once pruebas de las cuales dos fallan a propósito.

⏱ 17 min de lectura

Última lección del bloque, y cierra lo que abrió la lección 11: una API publicada no se puede cambiar.

Salvo que puedas, y para eso hay dos herramientas. El versionado, que permite cambiar sin romper. Y las pruebas, que te dicen si has roto algo.

Qué rompe y qué no#

Antes del cómo, el cuándo. No todos los cambios necesitan versión:

Cambio¿Rompe?
Añadir un campo a una respuestaNo
Añadir un punto de accesoNo
Añadir un parámetro opcionalNo
Hacer opcional un campo obligatorioNo
Quitar un campoSí
Renombrar un campoSí
Cambiar el tipo de un campoSí
Hacer obligatorio un campo opcionalSí
Cambiar un código de respuestaSí
Cambiar el formato de los erroresSí

La regla detrás de la tabla: añadir no rompe, quitar y cambiar sí. Un cliente que ignora los campos que no conoce sobrevive a lo primero.

Y hay dos casos que no parecen romper y rompen:

Cambiar el orden por defecto de un listado. Alguien que muestra los cinco primeros verá otros cinco.

Ajustar una validación para que sea más estricta. Datos que antes se aceptaban dejan de aceptarse, y del lado del cliente eso son errores nuevos en código que no ha cambiado.

Fíjate también en que hay un cambio de la tabla que se hace sin querer constantemente: añadir un campo no rompe, y con fields = '__all__' ocurre cada vez que alguien toca el modelo. Ahí el riesgo no es romper sino publicar de más, que es la lección 14.

Los cinco esquemas de DRF#

python
REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning',
    'DEFAULT_VERSION': 'v1',
    'ALLOWED_VERSIONS': ['v1', 'v2'],
}
EsquemaCómo pide el cliente la versión
URLPathVersioning/api/v1/libros/
NamespaceVersioningIgual, con espacios de nombres de Django
AcceptHeaderVersioningAccept: application/json; version=1.0
QueryParameterVersioning/api/libros/?version=1.0
HostNameVersioningv1.api.ejemplo.es

En la ruta es lo más común y lo que conviene por defecto. Se ve, se copia, se prueba con el navegador y sale en los registros. Su pega teórica es que el mismo recurso tiene dos direcciones, lo que a los puristas de REST les molesta; su ventaja práctica es que todo el mundo lo entiende.

En la cabecera es lo más correcto —la dirección identifica el recurso y la versión es una preferencia— y es lo que usa GitHub. Y es invisible: no se puede probar pegando una dirección en el navegador, no sale en los registros sin configurarlo, y quien consuma la API se olvidará de mandarla y acabará en la versión por defecto sin saberlo.

En un parámetro es lo más fácil de probar y lo que peor envejece, porque se mezcla con los filtros.

En el nombre del servidor solo tiene sentido si además vas a desplegarlas por separado.

La recomendación práctica: en la ruta, salvo que ya tengas una razón para lo contrario.

Dentro de la vista, la versión está disponible:

python
def get_serializer_class(self):
        if self.request.version == 'v2':
                return LibroSerializerV2
    return LibroSerializerV1

Probar una API#

DRF trae su propio cliente, que es el de Django sabiendo de JSON:

python
from rest_framework.test import APITestCase


class CatalogoTest(APITestCase):
        @classmethod
    def setUpTestData(cls):
                cls.ana = User.objects.create_user('ana', 'a@t.es', 'clave-larga-123')
        cls.ed = Editorial.objects.create(nombre='Anagrama', pais='España')
        cls.autor = Autor.objects.create(nombre='Óscar Núñez')
        for i in range(30):
                        libro = Libro.objects.create(titulo=f'Libro {i}', isbn=f'978-{i:05d}',
                                         precio='10.00', editorial=cls.ed)
            libro.autores.add(cls.autor)

    def setUp(self):
                self.client.force_authenticate(self.ana)

Cuatro cosas de esas líneas:

force_authenticate salta la autenticación entera. No comprueba tokens ni contraseñas: pone el usuario. Es lo que quieres en las pruebas que no van de autenticación, y significa que hay que probar la autenticación aparte.

setUpTestData crea los datos una vez para toda la clase, no una por prueba, que es la lección 39 del módulo 4.

format='json' en cada petición con cuerpo. Sin él, el cliente manda un formulario, y eso cambia cómo se interpretan las listas y los valores nulos, de modo que la prueba pasa por un camino distinto al del cliente real.

El autor tiene tildes a propósito. Es la diferencia entre unas pruebas que detectan el problema de la lección 19 y unas que no.

Lo que hay que probar#

El resultado de las once pruebas del proyecto:

Salida en pantalla
test_anonimo_no_entra ................................ ok
test_buscar_con_tilde ................................ ok
test_buscar_en_minuscula ............................. ok
test_buscar_sin_tilde ................................ FAIL
test_crear ........................................... ok
test_crear_sin_datos_da_400_por_campo ................ ok
test_el_listado_no_crece_con_las_filas ............... ok
test_el_precio_viaja_como_texto ...................... ok
test_isbn_repetido ................................... ok
test_listar .......................................... ok
test_patch_con_errata_no_cambia_nada ................. FAIL

Las dos que fallan son las importantes, y fallan a propósito:

Salida en pantalla
FAIL: test_buscar_sin_tilde
AssertionError: 0 != 30 : la búsqueda no encuentra sin tilde

FAIL: test_patch_con_errata_no_cambia_nada
AssertionError: 'Libro 0' != 'NUEVO' : el PATCH con errata contesta 200 sin guardar

Son los dos fallos silenciosos de las lecciones 19 y 13, y ninguno de los dos lo detecta una prueba normal, porque los dos devuelven 200.

Y hay un matiz que se ve comparando las tres pruebas de búsqueda: Núñez funciona, núñez en minúscula también, y Nunez sin tildes no. La segunda pasa porque la única letra que hay que convertir a minúscula es la N, que es ASCII. Con Óscar no pasaría, porque habría que convertir la Ó. Es el mismo problema asomando de dos maneras según la palabra, que es lo que lo hace difícil de diagnosticar.