Week 12 / 2023

Mohamed Saif published on
5 min, 857 words

Tags: pytest

Inroduction to Pytest

Features
  • Auto-discovery of test modules and functions
  • Modular fixtures for managing small or parametrized long-lived test resources
  • Can run unittest
  • Rich plugin architecture, with a growing ecosystem of third-party plugins
  • Pytest provides an option to run tests in parallel, which can significantly reduce the overall test execution time. Running tests in parallel means that multiple test sessions are executed at the same time.
quickstart
def inc(x):
    return x + 1


def test_answer():
    assert inc(3) == 5
  • run test
pytest
Test Discovery
  • test file name: test\__.py or _\_test.py
  • functions name: test_
  • classes name: Test_

command line

  • pytest -vv verbose
  • pytest -x stop after first failure
  • pytest -k [keyword] only run tests with name matching the expression
  • pytest -m [mark] only run tests with mark matching the expression
  • pytest --tb=<type> This option allows you to choose the type of traceback that is displayed when a test fails. The default is "auto", which shows the shortest traceback that still shows all relevant lines of code. Other options include "long", "short", and "line".
  • pytest -s disable capture

Fixtures

  • @pytest.fixture decorator
  • fixtures are functions, which will run before each test function that requests it as an input argument:

@pytest.fixture
def some_data():
    return 42

def test_some_data(some_data):
    assert some_data == 42
  • Fixtures can be used to return any kind of value, not just data. For example, you can use fixtures to return a database connection. This is useful if you want to write tests that interact with a database.
  • Pytest provides several built-in fixtures that can be used in testing. Some of the most important and commonly used ones are:
  • 1: tmp_path, tmp_path_factory: These fixtures return a temporary directory path object which is unique to each test function. The returned object is a pathlib.Path object.
  • 2: monkeypatch: This fixture allows you to modify objects, dictionaries, or os.environ (to modify environment variables) during a test. This is useful if you want to simulate different scenarios, such as testing what happens when an environment variable is not set. Or simulating a request to a server that returns a specific response. Or invode code which cannot be easily tested such as network access. All modifications will be undone after the requesting test function or fixture has finished.
Setup and Teardown: unit vs pytest
import asyncpg
import unittest

class TestPostgreSql(unittest.TestCase):
    async def setUp(self):
        self.conn = await asyncpg.connect(
            user="postgres",
            password="mohamed98",
            database="postgres",
            host="localhost",
            port=5432
        )

    async def tearDown(self):
        await self.conn.close()

    async def test_postgres_query(self):
        result = await self.conn.fetchval("SELECT 2 + 2")
        self.assertEqual(result, 4)
@pytest_asyncio.fixture()
async def postgres_db():
    conn = await asyncpg.connect(
        user="postgres",
        password="mohamed98",
        database="postgres",
        host="localhost",
        port=5432
    )

    yield conn

    await conn.close()

Parametrizing fixtures
  • Pytest parametrization is a feature that allows you to run the same test code multiple times with different inputs or parameters. It can help reduce code duplication and simplify test code, making it more readable and easier to maintain.
  • @pytest.mark.parametrize decorator.

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

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


# fixture parametrization
import pytest

@pytest.fixture(params=[(2, 2), (5, 10), (0, 0), (-1, 1)])
def input_values(request):
    return request.param

def test_add_numbers(input_values):
    a, b = input_values
    result = add_numbers(a, b)
    assert result == a + b

Pydantic

  • pydantic enforces type hints at runtime, and provides user friendly errors when data is invalid.
  • pydantic guarantees that the fields of the resultant model instance will conform to the field types defined on the model.
  • pydantic is primarily a parsing library, not a validation library. In other words, pydantic guarantees the types and constraints of the output model, not the input data.
  • Pydantic is no a validation library, it's a parsing library.
  • It makes no guarantee whatsoever about checking the form of data you give to it; instead it makes a guarantee about the form of data you get out of it.
  • Clarify the purpose of pydantic e.g. not strict validation #578
  • When __root__ is used as a field name, it indicates that the model has a single field that is not named explicitly, but is instead represented by the root of the model's structure. In other words, __root__ is used to indicate that the model's structure is a simple dictionary with no additional named fields.
  • validators can be used for parsing as well as validating.

Frappe

erpnext

  • the sales from the POS session do not affect the stock and accounting ledgers until a Closing POS Voucher is submitted for that session.
  • Each transaction from the POS screen now creates an intermediate invoice (called a POS Invoice)
  • closing a POS session by a single sales invoice which merges all the intermediate invoices created throughout the day.