Pytest Best Practices

7 buenas prácticas para escribir tests con Pytest: tests simples, mocks, parametrize, fixtures, testear comportamiento y no implementación, autospec y markers.
pytest
Python
best-practices
Published

June 9, 2023

1. Simple Test

  • Test one feature at a time
  • Small test
  • A single assertion

Example code

def some_calculation(a, b):
    return a + b


def make_a_dict(a, b):
    if a < 0 or b < 0:
        raise ValueError("a and b must be positive")

    operation = some_calculation(a, b)

    return {"a": a, "b": b, "result": operation}

Bad

def test_dict():
    assert make_a_dict(2, 3) == {"a": 2, "b": 3, "result": 5}
    with pytest.raises(ValueError):
         make_a_dict(-1, -1)

Good

def test_make_a_dict():
    my_dict = make_a_dict(2, 3)
    expected_dict = {"a": 2, "b": 3, "result": 5}
    assert my_dict == expected_dict


def test_make_a_dict_with_negative_values():
    with pytest.raises(ValueError):
        make_a_dict(-1, -1)

2. Mock everything we don’t want to test

Isolate functions from other functions.

Bad

def test_make_a_dict():
    my_dict = make_a_dict(2, 3)
    expected_dict = {"a": 2, "b": 3, "result": 5}
    assert my_dict == expected_dict

Good

def test_make_a_dict(mocker):
    mocker.patch(
        "__main__.some_calculation",
        return_value=5,
        autospec=True
    )

    my_dict = make_a_dict(2, 3)
    expected_dict = {"a": 2, "b": 3, "result": 5}
    assert my_dict == expected_dict


def test_some_calculation():
    value = some_calculation(2, 3)
    assert value == 5

3. DRY (Don’t repeat yourself)

@pytest.mark.parametrize

def add_numbers(a, b):
    return a + b


@pytest.mark.parametrize("a, b, expected_result", [
    (2, 3, 5),
    (-10, 5, -5),
    (0, 0, 0),
    (100, -50, 50)
])
def test_add_numbers(a, b, expected_result):
    result = add_numbers(a, b)
    assert result == expected_result

@pytest.fixture

class Person:
    def __init__(self, name):
        self.name = name
        self.age = 0

    def is_adult(self):
        return self.age >= 18

Bad

def test_person_is_adult():
    person = Person("Emi")
    person.age = 19
    assert person.is_adult()


def test_person_is_not_adult():
    person = Person("Emi")
    person.age = 10
    assert not person.is_adult()

Good

@pytest.fixture
def person():
    return Person("Emi")


def test_person_is_adult(person):
    person.age = 19
    assert person.is_adult()


def test_person_is_not_adult(person):
    person.age = 10
    assert not person.is_adult()

4. Test behavior, not implementation

from dataclasses import dataclass


@dataclass
class User:
    username: str


class InMemoryUserRepository:
    def __init__(self):
        self._users = []

    def add(self, user):
        self._users.append(user)

    def get_by_username(self, username):
        return next(user for user in self._users if user.username == username)

Bad — accede a internals del objeto

def test_add():
    user = User(username="johndoe")
    repository = InMemoryUserRepository()
    repository.add(user)
    assert user in repository._users  # accediendo a _users directamente

Good

def test_added_user_can_be_retrieved_by_username():
    user = User(username="johndoe")
    repository = InMemoryUserRepository()
    repository.add(user)
    assert user == repository.get_by_username(user.username)

5. Do not test the framework (at least in unit tests)

Bad — estás testeando Django/Flask, no tu código

@pytest.mark.django_db
def test_django_authentication():
    user = User.objects.create_user(username='testuser', password='testpassword')
    authenticated = user.check_password('testpassword')
    assert authenticated

6. autospec=True

Útil para verificar que una función es llamada con los argumentos correctos. Autospec genera un mock que respeta la firma original de la función.

def test_make_a_dict(mocker):
    mocker.patch(
        "__main__.some_calculation",
        return_value=5,
        autospec=True
    )

    my_dict = make_a_dict(2, 3)
    expected_dict = {"a": 2, "b": 3, "result": 5}
    assert my_dict == expected_dict

7. Mark tests: slow, integration, exhaustive

Usar markers para categorizar los tests y poder correr subconjuntos según el contexto.