Test Client: Streamlining Database Testing in Edgy¶
Have you ever struggled with testing your database interactions, ensuring your model tests target a specific test database instead of your development database? This is a common challenge, often requiring significant setup. Edgy addresses this with its built-in DatabaseTestClient, simplifying your database testing workflow.
Before proceeding, ensure you have the Edgy test client installed with the necessary dependencies:
$ pip install edgy[test]
DatabaseTestClient¶
The DatabaseTestClient is designed to streamline database testing, automating the creation and management of test databases.
from edgy.testclient import DatabaseTestClient
Parameters¶
- 
url: The database URL, either as a string or a
databases.DatabaseURLobject.from databases import DatabaseURL - 
force_rollback: Ensures all database operations are executed within a transaction that rolls back upon disconnection.
Default:
False - 
lazy_setup: Sets up the database on the first connection, rather than during initialization.
Default:
True - 
use_existing: Uses an existing test database if it was previously created and not dropped.
Default:
False - 
drop_database: Drops the test database after the tests have completed.
Default:
False - 
test_prefix: Allows a custom test database prefix. Leave empty to use the URL's database name with a default prefix.
Default:
testclient_default_test_prefix(defaults totest_) 
Configuration via Environment Variables¶
Most default parameters can be overridden using capitalized environment variables prefixed with EDGY_TESTCLIENT_.
For example: EDGY_TESTCLIENT_DEFAULT_PREFIX=foobar or EDGY_TESTCLIENT_FORCE_ROLLBACK=true.
This is particularly useful for configuring tests in CI/CD environments.
Usage¶
The DatabaseTestClient is designed to be familiar to users of Edgy's Database object, as it extends its functionality with testing-specific features.
Consider a database URL like this:
DATABASE_URL = "postgresql+asyncpg://postgres:postgres@localhost:5432/my_db"
In this case, the database name is my_db. When using the DatabaseTestClient, it automatically targets a test database named test_my_db.
Here's an example of how to use it in a test:
import datetime
import decimal
import ipaddress
import uuid
from datetime import date as local_date
from datetime import datetime as local_datetime
from datetime import time as local_time
from enum import Enum
from typing import Any, Dict
from uuid import UUID
import pytest
from tests.settings import DATABASE_URL
import edgy
from edgy.core.db import fields
from edgy.testclient import DatabaseTestClient
database = DatabaseTestClient(DATABASE_URL, drop_database=True)
models = edgy.Registry(database=edgy.Database(database, force_rollback=True))
pytestmark = pytest.mark.anyio
def time():
    return datetime.datetime.now().time()
class StatusEnum(Enum):
    DRAFT = "Draft"
    RELEASED = "Released"
class Product(edgy.Model):
    # autogenerated for models without primary key
    id: int = fields.IntegerField(primary_key=True, autoincrement=True)
    uuid: UUID = fields.UUIDField(null=True)
    created: local_datetime = fields.DateTimeField(default=datetime.datetime.now)
    created_day: local_date = fields.DateField(default=datetime.date.today)
    created_time: local_time = fields.TimeField(default=time)
    created_date: local_date = fields.DateField(auto_now_add=True)
    created_datetime: local_datetime = fields.DateTimeField(auto_now_add=True)
    updated_datetime: local_datetime = fields.DateTimeField(auto_now=True)
    updated_date: local_date = fields.DateField(auto_now=True)
    data: Dict[str, Any] = fields.JSONField(default={})
    description: str = fields.CharField(default="", max_length=255)
    huge_number: int = fields.BigIntegerField(default=0)
    price: decimal.Decimal = fields.DecimalField(max_digits=5, decimal_places=2, null=True)
    status: Enum = fields.ChoiceField(StatusEnum, default=StatusEnum.DRAFT)
    value: float = fields.FloatField(null=True)
    class Meta:
        registry = models
class User(edgy.Model):
    id: int = fields.UUIDField(primary_key=True, default=uuid.uuid4)
    name: str = fields.CharField(null=True, max_length=16)
    email: str = fields.EmailField(null=True, max_length=256)
    ipaddress: str = fields.IPAddressField(null=True)
    url: str = fields.URLField(null=True, max_length=2048)
    password: str = fields.PasswordField(null=True, max_length=255)
    class Meta:
        registry = models
class Customer(edgy.Model):
    name: str = fields.CharField(null=True, max_length=16)
    class Meta:
        registry = models
@pytest.fixture(autouse=True, scope="module")
async def create_test_database():
    # this creates and drops the database
    async with database:
        await models.create_all()
        yield
        await models.drop_all()
@pytest.fixture(autouse=True, scope="function")
async def rollback_transactions():
    # this rolls back
    async with models:
        yield
async def test_model_crud():
    product = await Product.query.create()
    product = await Product.query.get(pk=product.pk)
    assert product.created.year == datetime.datetime.now().year
    assert product.created_day == datetime.date.today()
    assert product.created_date == datetime.date.today()
    assert product.created_datetime.date() == datetime.datetime.now().date()
    assert product.updated_date == datetime.date.today()
    assert product.updated_datetime.date() == datetime.datetime.now().date()
    assert product.data == {}
    assert product.description == ""
    assert product.huge_number == 0
    assert product.price is None
    assert product.status == StatusEnum.DRAFT
    assert product.value is None
    assert product.uuid is None
    await product.update(
        data={"foo": 123},
        value=123.456,
        status=StatusEnum.RELEASED,
        price=decimal.Decimal("999.99"),
        uuid=uuid.UUID("f4e87646-bafa-431e-a0cb-e84f2fcf6b55"),
    )
    product = await Product.query.get()
    assert product.value == 123.456
    assert product.data == {"foo": 123}
    assert product.status == StatusEnum.RELEASED
    assert product.price == decimal.Decimal("999.99")
    assert product.uuid == uuid.UUID("f4e87646-bafa-431e-a0cb-e84f2fcf6b55")
    last_updated_datetime = product.updated_datetime
    last_updated_date = product.updated_date
    user = await User.query.create()
    assert isinstance(user.pk, uuid.UUID)
    user = await User.query.get()
    assert user.email is None
    assert user.ipaddress is None
    assert user.url is None
    await user.update(
        ipaddress="192.168.1.1",
        name="Test",
        email="test@edgy.com",
        url="https://edgy.com",
        password="12345",
    )
    user = await User.query.get()
    assert isinstance(user.ipaddress, (ipaddress.IPv4Address, ipaddress.IPv6Address))
    assert user.password == "12345"
    assert user.url == "https://edgy.com"
    await product.update(data={"foo": 1234})
    assert product.updated_datetime != last_updated_datetime
    assert product.updated_date == last_updated_date
Explanation¶
This example demonstrates a test using DatabaseTestClient. The client ensures that all database operations within the test are performed on a separate test database, test_my_db in this case.
The drop_database=True parameter ensures that the test database is deleted after the tests have finished running, preventing the accumulation of test databases.
This approach provides a clean and isolated testing environment, ensuring that your tests do not interfere with your development or production databases.