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():
    """
    Test the make_a_dict function to ensure it returns the expected dictionary.
    """
    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():
    """
    Test that make_a_dict raises a ValueError when negative values are passed.
    """
    with pytest.raises(ValueError):
        make_a_dict(-1, -1)

2. Mock everything we don’t want to test

  • isolate functions from other functions

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_make_a_dict():
    """
    Test the make_a_dict function to ensure it returns the expected dictionary.
    """
    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):
    """
    Test the make_a_dict function to ensure it returns the expected dictionary.
    """
    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():
    """
    Test the some_calculation function to ensure it returns the expected value.
    """
    value = some_calculation(2, 3)

    expected_value = 5

    assert value == expected_value

3. DRY (Don’t repeat yourself)

🔥 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

🔥 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 = Person("Emi")
    person.age = 19
    assert person.is_adult()


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

✅ Good

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


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

Example code

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

def test_add():
    user = User(username="johndoe")
    repository = InMemoryUserRepository()
    repository.add(user)

    assert user in repository._users


def test_get_by_username():
    user = User(username="johndoe")
    repository = InMemoryUserRepository()
    repository._users = [user]

    user_from_repository = repository.get_by_username(user.username)

    assert user_from_repository == user

✅ 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

from django.contrib.auth.models import User


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


def test_flask_error_handling():
    app = Flask(__name__)

    with app.test_client() as client:
        response = client.get('/nonexistent-route')
        assert response.status_code == 404
        assert 'Not Found' in response.get_data(as_text=True)

6. Autospect True

This is useful when you want to verify or inspect specific features of a function or method during testing. For example, Autospec True can be used to check if a function is called with the correct arguments, if it returns the expected values, or if the appropriate methods of an object are called.

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}

✅ Good

def test_make_a_dict(mocker):
    """
    Test the make_a_dict function to ensure it returns the expected dictionary.
    """
    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 test slow, integrationist, exhaustive

  • https://docs.pytest.org/en/7.1.x/example/markers.html

Share on: TwitterFacebookEmail

Comments

comments powered by Disqus

Related Posts


Published

Category

python

Tags

Contact