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.DatabaseURL
object.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=database)
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():
await models.create_all()
yield
await models.drop_all()
@pytest.fixture(autouse=True)
async def rollback_transactions():
with database.force_rollback():
async with database:
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.