Loading Likes...

Hoy quiero enseñaros cómo hemos resuelto el problema de escribir tests para código de negocio que requieren de una base de datos para su funcionamiento real, pero sin conectarnos a ella para ejecutarlos.

Es habitual en este tipo de casos recurrir a usar una base de datos específica para tests y cargarla con datos de prueba (comúnmente llamados fixtures). El problema con esta estrategia es que nos obliga a depender de una base de datos para ejecutar los tests y a mantener estos fixtures actualizados, lo cual resulta engorroso.

Todo empieza con la manera en la que usamos SQLAlchemy. En lugar de usar directamente el objeto session de SQLAlchemy para realizar consultas, hacemos uso de una clase Manager asociada a cada modelo. Estos managers se inyectan a cada modelo automáticamente mediante una metaclase de Python. Se entiende más fácil con un ejemplo:

# Crear un objeto usando SQLAlchemy directamente
session.add(Business(name="Habitissimo")).save()
# Buscarlo en la base de datos con SQLAlchemy
session.query(Business).filter(
    Business.name == "Habitissimo").scalar()

# Usando el manager
Business.objects.create(name="Habitissimo")
Business.objects.get(Business.name == "Habitissimo")

La idea te resultará familiar si has trabajado con el ORM de Django. De hecho, está completamente inspirada en él.

La implementación del manager la podéis encontrar al final del post; para usarla, simplemente se define el modelo usando la clase Model como base.

class Business(Model):
    __tablename__ = 'businesses'
    id = Column(Integer, primary_key=True)
    name = Column(Text)

Vamos a poner un ejemplo de código que requiere de la base de datos para completarse y veremos cómo podemos usar esto para probarlo.

def get_or_create_category(category_slug):
    """
    find a category from its slug, if the category does not exits
    creates one and puts it under the "orphan" root object.
    """
    category = Category.objects.get(Category.slug == category_slug)
    if not category:
        root = Category.objects.get(Category.name == 'orphan')
        category = Category.objects.create(
            name=category_slug,
            slug=category_slug,
            parent_id=root.id
        )
    return category

Este ejemplo real de una aplicación busca una categoría por su slug (nombre normalizado para URL) y, en caso de no encontrarlo, lo crea bajo una categoría especial llamada “huérfanos”, donde luego serán procesados por una persona que indicará bajo qué categoría final deben ubicarse. Vamos a ver cómo queda el test para esta función:

from unittest.mock import patch, call

def test_get_category_by_slug(self):
    with patch.object(Category, 'objects') as objects:
      objects.get.side_effect = [None, Category(id=0)]
      get_or_create_category('foo')
      assert objects.mock_calls == [
        call.get(C(Category.slug == 'foo')),
        call.get(C(Category.name == 'orphan')),
        call.create(name='foo', slug='foo', parent_id=0)
      ]

Si has leído atentamente el ejemplo, verás que aparece una clase llamada C y te preguntarás cuál es su finalidad.

Pues bien, en SQLAlchemy la expresión Category.slug == 'foo' crea un objeto del tipo BinaryExpression. Estos objetos sobrescriben el operador de igualdad de python __eq__, por lo que si intentamos compararlo con otra expresión recibimos un nuevo BinaryExpression en lugar de un valor booleano. Esto no es lo que queremos, pero es un mecanismo de SQLAlchemy que permite definir condiciones complejas para las búsquedas en la base de datos. Para comparar dos expresiones entre sí podemos usar el método compare, por lo que la siguiente expresión sí resulta en un valor booleano como el que esperábamos: (Category.slug == 'foo').compare(Category.slug == 'bar'). En este caso, la comparación resulta en un valor negativo False.

Nuestro test requiere que dos expresiones equivalentes puedan ser comparadas usando el comparador de igualdad estándar. Para ello hemos definido la clase C que actúa como envoltorio de una expresión sobrecargando de nuevo el operador.

Lo que nos permite expresar la comparación de la siguiente manera:

C(Category.slug == 'foo') == (Category.slug == 'bar')

Para terminar, os dejo con la implementación de Model y Manager:

Loading Likes...

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *